-- ============================================================ -- sync.lua – Persistent vehicle management (server-side) -- ============================================================ PersistentVehicles = {} SpawnTimeout = nil -- ────────────────────────────────────────────── -- Helpers -- ────────────────────────────────────────────── local function NormalizeVehicleType(vehicleType) local map = { car = "vehicle", automobile = "vehicle", pdm = "vehicle", airplane = "plane", air = "plane", helicopter = "plane", heli = "plane", water = "boat", } return map[vehicleType] or vehicleType end local function GetVehicleCoordsFromProps(vehicleRow) local propsColumn = garagePropsColumn local rawProps = vehicleRow[propsColumn] local props = rawProps and json.decode(rawProps) if props then return props.coords end return nil end local function ImpoundAndDeleteIfSpawned(plate, impoundGarage, statusValue) local query = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ?", garageTable, storedColumn ) MySQL.Sync.execute(query, { impoundGarage, statusValue, plate }) local vehicleKey = GetVehicleFromPlate(plate) if vehicleKey and PersistentVehicles[vehicleKey] then local entity = PersistentVehicles[vehicleKey].entity if DoesEntityExist(entity) then DeleteEntity(entity) end PersistentVehicles[vehicleKey] = nil end end -- Deep-clone a table recursively local function DeepClone(tbl) local clone = table.clone(tbl) for key, value in pairs(clone) do if type(value) == "table" then clone[key] = DeepClone(value) end end return clone end -- ────────────────────────────────────────────── -- GetVehicleFromPlate (key → index in PersistentVehicles) -- ────────────────────────────────────────────── function GetVehicleFromPlate(plate) for key, vehicleData in pairs(PersistentVehicles) do if vehicleData.vehicle and vehicleData.vehicle.plate == plate then return key end end return nil end -- ────────────────────────────────────────────── -- GetPlayersFromRoutingZero -- ────────────────────────────────────────────── function GetPlayersFromRoutingZero() local routingZeroPlayers = {} for _, playerSource in pairs(Players) do if GetPlayerRoutingBucket(playerSource) == 0 then routingZeroPlayers[#routingZeroPlayers + 1] = playerSource end end local allInRoutingZero = #routingZeroPlayers == #Players return routingZeroPlayers, allInRoutingZero end -- ────────────────────────────────────────────── -- SpawnVehicle -- ────────────────────────────────────────────── function SpawnVehicle(dbId, ownerIdentifier, vehicleType, spawnCoords, vehicleProps, targetPlayer, setKeys) vehicleType = NormalizeVehicleType(vehicleType) if not CreateVehicleServerSetter then print("[GARAGES] ERREUR CRITIQUE: La fonction CreateVehicleServerSetter n'existe pas sur votre serveur !") return false end local vehicle = CreateVehicleServerSetter(vehicleProps.model, vehicleType, spawnCoords.x, spawnCoords.y, spawnCoords.z, spawnCoords.w or 0.0) if not DoesEntityExist(vehicle) then Error("SpawnVehicle ::: Failed to create vehicle entity. Model:", vehicleProps.model) return false end local netId = NetworkGetNetworkIdFromEntity(vehicle) local plateStr = MathTrim(vehicleProps.plate) SetVehicleNumberPlateText(vehicle, plateStr) local persistentEntry = { id = dbId, type = vehicleType, entity = vehicle, netID = netId, vehicle = vehicleProps, plate = plateStr, owner = ownerIdentifier, } PersistentVehicles[plateStr] = persistentEntry if setKeys and targetPlayer then TriggerClientEvent("advancedgarages:giveKeys", targetPlayer, netId, plateStr) end Debug("SpawnVehicle ::: Spawned vehicle. Plate:", plateStr, "NetID:", netId) return persistentEntry end exports("SpawnVehicle", SpawnVehicle) -- ────────────────────────────────────────────── -- setVehicleToPersistent (export) -- ────────────────────────────────────────────── function setVehicleToPersistent(netId) local entity = NetworkGetEntityFromNetworkId(netId) local plate = MathTrim(GetVehicleNumberPlateText(entity)) local query = "SELECT * FROM " .. garageTable .. " WHERE plate = ?" local result = MySQL.Sync.fetchAll(query, { plate }) if not result[1] then print("setVehicleToPersistent ::: The vehicle is not in the database") return false end local row = result[1] local vehicleCol = (Config.Framework == "qb") and "mods" or "vehicle" local props = json.decode(row[vehicleCol]) if not props then print("setVehicleToPersistent ::: Does not vehicle data exist vehicle col is", vehicleCol) return false end PersistentVehicles[plate] = { id = row.id, type = row.type, entity = entity, netID = netId, vehicle = props, plate = plate, owner = row.owner, } return true end exports("setVehicleToPersistent", setVehicleToPersistent) -- ────────────────────────────────────────────── -- removeVehicleFromPersistent (export) -- ────────────────────────────────────────────── function removeVehicleFromPersistent(plate, targetGarage) local vehicleKey = GetVehicleFromPlate(plate) if not vehicleKey then Error("removeVehicleFromPersistent ::: Vehicle is not persistent or spawned, plate:", plate) return false end local statusValue = State.stored if not targetGarage then Debug("No garage was specified trying to send the vehicle to the impound") local vehicleData = PersistentVehicles[vehicleKey] targetGarage = GetImpoundOfType(vehicleData.type, vehicleData.vehicle.coords) if not targetGarage then Error("No impound for type:", vehicleData.type, "coords", vehicleData.vehicle.coords) return false end statusValue = State.impounded end local vehicleProps = PersistentVehicles[vehicleKey].vehicle local query = string.format( "UPDATE %s SET `garage` = ?, `%s` = ?, `%s` = ? WHERE plate = ?", garageTable, storedColumn, garagePropsColumn ) MySQL.Sync.execute(query, { targetGarage, statusValue, json.encode(vehicleProps), plate }) Debug("Vehicle saved in garage:", targetGarage, "plate:", plate) local entity = PersistentVehicles[vehicleKey].entity if DoesEntityExist(entity) then DeleteEntity(entity) end PersistentVehicles[vehicleKey] = nil return true end exports("removeVehicleFromPersistent", removeVehicleFromPersistent) -- ────────────────────────────────────────────── -- Debug command: /addtopers -- ────────────────────────────────────────────── RegisterCommand("addtopers", function(sourcePlayer) local ped = GetPlayerPed(sourcePlayer) local vehicle = GetVehiclePedIsIn(ped, false) if not DoesEntityExist(vehicle) then return end local netId = NetworkGetNetworkIdFromEntity(vehicle) local result = exports["qs-advancedgarages"]:setVehicleToPersistent(netId) if result then print("yay") end end, false) -- ────────────────────────────────────────────── -- Debug command: /tptoplate -- ────────────────────────────────────────────── RegisterCommand("tptoplate", function(sourcePlayer, args) if not PlayerIsAdmin(sourcePlayer) then return print("You are not admin") end if not args then return print("No plate") end local plate = table.concat(args, " ") local vehicleKey = GetVehicleFromPlate(plate) if not vehicleKey then return print("Vehicle not found") end local coords = PersistentVehicles[vehicleKey].vehicle.coords SetEntityCoords(GetPlayerPed(sourcePlayer), coords.x, coords.y, coords.z) Debug("Teleported to:", plate) end, false) -- ────────────────────────────────────────────── -- Startup: send "OUT" garage vehicles to impound -- (PersistentVehicles mode) -- ────────────────────────────────────────────── if Config.PersistentVehicles then CreateThread(function() Wait(1000) local selectQuery = string.format("SELECT * FROM %s WHERE garage = 'OUT'", garageTable) local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ?", garageTable, storedColumn ) local spawnedPlates = {} -- Wait until at least one player in routing bucket 0 while true do if initializedStartup then break end local players, allInZero = GetPlayersFromRoutingZero() if not allInZero then Debug("Not any player has routing 0, waiting...") end if #players > 0 then initializedStartup = true local outVehicles = MySQL.Sync.fetchAll(selectQuery) for _, row in pairs(outVehicles) do local ownerIdent = row[garageIdentifierColumn] if Config.Framework == "qb" then row.vehicle = row.mods end local props = json.decode(row.vehicle) if props then row.type = NormalizeVehicleType(row.type) local propsCoords = json.decode(row[garagePropsColumn] or "") local vehicleCoords = (propsCoords and propsCoords.coords) or nil local impound = GetImpoundOfType(row.type, vehicleCoords) if not impound then Debug("No impound for type:", row.type) elseif not props.coords then MySQL.Sync.execute(updateQuery, { impound, State.impounded, row.plate }) Debug("Vehicle without coordinates:", row.plate, ", sent to:", impound) else local spawnedEntry = SpawnVehicle(row.id, ownerIdent, row.type, props.coords, props) table.insert(spawnedPlates, spawnedEntry) end end end break end Wait(5000) end TriggerClientEvent("advancedgarages:updatePersistentVehicleBlips", -1, PersistentVehicles) end) else -- Non-persistent mode: send all OUT vehicles to impound at startup CreateThread(function() local count = 0 local selectQuery = string.format("SELECT * FROM %s WHERE garage = 'OUT'", garageTable) local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE garage = 'OUT'", garageTable, storedColumn ) local outVehicles = MySQL.Sync.fetchAll(selectQuery) for _, row in pairs(outVehicles) do local vehicleCoords = GetVehicleCoordsFromProps(row) local impound = GetImpoundOfType(row.type, vehicleCoords) local isSpawned = GetVehicleFromPlate(row.plate) if not impound then Debug("No impound for type:", row.type) elseif isSpawned then Debug("Vehicle:", row.plate, " was found in the server, we don't send it to the impound.") else count = count + 1 MySQL.Sync.execute(updateQuery, { impound, State.impounded }) end end if count > 0 then Debug("All vehicles in the OUT garage have been sent to their respective impound. Total: " .. count) end end) end -- Startup: send "OUT" plane-type vehicles to impound CreateThread(function() local count = 0 local selectQuery = string.format("SELECT * FROM %s WHERE garage = 'OUT' AND type = 'plane'", garageTable) local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE garage = 'OUT' AND type = 'plane'", garageTable, storedColumn ) local outPlanes = MySQL.Sync.fetchAll(selectQuery) for _, row in pairs(outPlanes) do local vehicleCoords = GetVehicleCoordsFromProps(row) local impound = GetImpoundOfType(row.type, vehicleCoords) local isSpawned = GetVehicleFromPlate(row.plate) if not impound then Debug("No impound for type:", row.type) elseif isSpawned then -- vehicle already on server, skip else count = count + 1 MySQL.Sync.execute(updateQuery, { impound, State.impounded }) end end end) -- ────────────────────────────────────────────── -- GetNearbyPlayerFromCoords -- ────────────────────────────────────────────── local function GetNearbyPlayerFromCoords(coords) local position = vec3(coords.x, coords.y, coords.z) local maxRadius = 70.0 for _, playerSource in pairs(Players) do local ped = GetPlayerPed(playerSource) if GetEntityRoutingBucket(ped) == 0 then local pedCoords = vec3(table.unpack({ GetEntityCoords(ped) })) if #(position - pedCoords) < maxRadius then return playerSource end end end return nil end -- ────────────────────────────────────────────── -- vehiclePropertiesSetted event -- ────────────────────────────────────────────── RegisterNetEvent("advancedgarages:vehiclePropertiesSetted") AddEventHandler("advancedgarages:vehiclePropertiesSetted", function(plate) local vehicleKey = GetVehicleFromPlate(plate) if not vehicleKey then Debug("Vehicle does not exist, plate:", plate, "persistentVehicles", PersistentVehicles) return end PersistentVehicles[vehicleKey].propertiesSetted = true end) -- ────────────────────────────────────────────── -- Persistent vehicle tick (properties + respawn) -- ────────────────────────────────────────────── local function TickPersistentVehicles() local propertyQueue = {} local respawnList = {} local respawnedPlates = {} for key, vehicleData in pairs(PersistentVehicles) do if not DoesEntityExist(vehicleData.entity) then Debug("The vehicle does not exist, trying to spawn it again...", vehicleData.plate) table.insert(respawnList, { index = key, id = vehicleData.id, owner = vehicleData.owner, vType = vehicleData.type, coords = vehicleData.vehicle.coords, props = vehicleData.vehicle, plate = vehicleData.plate, deleteEntity = false, entity = vehicleData.entity, }) else -- Update live state local entityCoords = GetEntityCoords(vehicleData.entity) local entityHeading = GetEntityHeading(vehicleData.entity) PersistentVehicles[key].vehicle.coords = vec4(entityCoords.x, entityCoords.y, entityCoords.z, entityHeading) local entityState = Entity(vehicleData.entity).state PersistentVehicles[key].vehicle.deformations = entityState.deformations or {} if Config.Fuel == "none" then PersistentVehicles[key].vehicle.fuel = entityState.fuel or 100.0 end PersistentVehicles[key].vehicle.engineHealth = GetVehicleEngineHealth(vehicleData.entity) PersistentVehicles[key].vehicle.bodyHealth = GetVehicleBodyHealth(vehicleData.entity) PersistentVehicles[key].vehicle.locked = GetVehicleDoorLockStatus(vehicleData.entity) -- Handle property retries if not vehicleData.propertiesSetted then PersistentVehicles[key].propertySetRetries = (PersistentVehicles[key].propertySetRetries or 0) + 1 local retries = PersistentVehicles[key].propertySetRetries if retries > 15 then Debug("Property set retries exceeded for plate:", vehicleData.plate, "- force respawning") table.insert(respawnList, { index = key, id = vehicleData.id, owner = vehicleData.owner, vType = vehicleData.type, coords = vehicleData.vehicle.coords, props = vehicleData.vehicle, plate = vehicleData.plate, deleteEntity = true, entity = vehicleData.entity, }) else local trimmedPlate = MathTrim(vehicleData.plate) if not trimmedPlate then Debug("There was a problem loading the plate for said vehicle...") return end SetVehicleNumberPlateText(vehicleData.entity, trimmedPlate) local nearbyPlayer = GetNearbyPlayerFromCoords(vehicleData.vehicle.coords) if nearbyPlayer then local clonedData = DeepClone(vehicleData) clonedData.source = nearbyPlayer if not propertyQueue[nearbyPlayer] then propertyQueue[nearbyPlayer] = {} end table.insert(propertyQueue[nearbyPlayer], clonedData) end end end end end -- Respawn vehicles if #respawnList > 0 then for _, entry in ipairs(respawnList) do if entry.deleteEntity and entry.entity then if DoesEntityExist(entry.entity) then DeleteEntity(entry.entity) end end PersistentVehicles[entry.index] = nil SpawnVehicle(entry.id, entry.owner, entry.vType, entry.coords, entry.props) table.insert(respawnedPlates, entry.plate) end if #respawnedPlates > 0 then Debug("The following vehicles have been respawned:", table.concat(respawnedPlates, ", ")) TriggerClientEvent("advancedgarages:updatePersistentVehicleBlips", -1, PersistentVehicles) end end -- Send property requests to nearby players if type(propertyQueue) == "table" and next(propertyQueue) ~= nil then for playerSource, vehicleList in pairs(propertyQueue) do TriggerClientEvent("advancedgarages:setVehicleProperties", playerSource, vehicleList) end end end -- ────────────────────────────────────────────── -- updatePersistentVehicleProps (export) -- ────────────────────────────────────────────── function updatePersistentVehicleProps(plate, newProps) assert(type(plate) == "string", "updatePersistentVehicleProps ::: Plate must be a string") local vehicleKey = GetVehicleFromPlate(plate) assert(vehicleKey, "updatePersistentVehicleProps ::: Vehicle not found for plate: " .. plate) local entity = PersistentVehicles[vehicleKey].entity assert(DoesEntityExist(entity), "updatePersistentVehicleProps ::: Entity does not exist for vehicle: " .. plate) PersistentVehicles[vehicleKey].vehicle = newProps SaveAllPersistentVehicles() Debug("updatePersistentVehicleProps ::: Updating vehicle properties for plate:", plate) return true end exports("updatePersistentVehicleProps", updatePersistentVehicleProps) -- ────────────────────────────────────────────── -- getNearbyVehicle – find a vehicle near player -- checks PersistentVehicles first, then all vehicles -- ────────────────────────────────────────────── local function FindNearbyVehicleFromAllVehicles(sourcePlayer) local pedCoords = GetEntityCoords(GetPlayerPed(sourcePlayer)) for _, vehicleEntity in pairs(GetAllVehicles()) do local coords = GetEntityCoords(vehicleEntity) if not coords then print("No GetEntityCoords") return end if #(pedCoords - vec3(coords.x, coords.y, coords.z)) < 4 then local plate = MathTrim(GetVehicleNumberPlateText(vehicleEntity)) local dbResult = MySQL.Sync.fetchAll( "SELECT * FROM " .. garageTable .. " WHERE plate = @plate", { ["@plate"] = plate } ) if dbResult[1] then local vehicleType = NormalizeVehicleType(dbResult[1].type) return vehicleEntity, vehicleType end end end return false end local function FindNearbyPersistentVehicle(sourcePlayer) local pedCoords = GetEntityCoords(GetPlayerPed(sourcePlayer)) for key, vehicleData in pairs(PersistentVehicles) do local vehicleCoords = vehicleData.vehicle.coords if not vehicleCoords then -- Try to get coords from entity if DoesEntityExist(vehicleData.entity) then local ec = GetEntityCoords(vehicleData.entity) local eh = GetEntityHeading(vehicleData.entity) vehicleData.vehicle.coords = vec4(ec.x, ec.y, ec.z, eh) vehicleCoords = vehicleData.vehicle.coords else print("This vehicle is not exist but tried to impound wtf", vehicleData.plate) end end if vehicleCoords then local dist = #(pedCoords - vec3(vehicleCoords.x, vehicleCoords.y, vehicleCoords.z)) vehicleData.type = NormalizeVehicleType(vehicleData.type) if dist < 4 then return key, vehicleData.type end end end return FindNearbyVehicleFromAllVehicles(sourcePlayer) end lib.callback.register("advancedgarages:getNearbyVehicle", function(sourcePlayer) return FindNearbyPersistentVehicle(sourcePlayer) end) -- ────────────────────────────────────────────── -- Delete nearby vehicle (admin command + export) -- ────────────────────────────────────────────── local function DeleteNearbyVehicle(sourcePlayer) local jobName = GetJobName(sourcePlayer) local isAdmin = PlayerIsAdmin(sourcePlayer) local isAllowed = IsJobAllowed(jobName, "impound") if not isAllowed and not isAdmin then return end local vehicleKey, vehicleType = FindNearbyPersistentVehicle(sourcePlayer) -- No persistent vehicle nearby → try deleting any nearby entity if not vehicleKey then local pedCoords = GetEntityCoords(GetPlayerPed(sourcePlayer)) local nearest = nil local minDist = 30.0 for _, vehicleEntity in pairs(GetAllVehicles()) do local coords = GetEntityCoords(vehicleEntity) local dist = #(pedCoords - vec3(coords.x, coords.y, coords.z)) if dist < minDist then minDist = dist nearest = vehicleEntity end end if nearest then if DoesEntityExist(nearest) then DeleteEntity(nearest) end Notification(sourcePlayer, i18n.t("vehicle_removed"), "success") else Notification(sourcePlayer, i18n.t("no_vehicle_nearby"), "info") end return end -- Found persistent vehicle → impound it local persistentData = PersistentVehicles[vehicleKey] local plate = persistentData and persistentData.vehicle and persistentData.vehicle.plate if not plate then plate = MathTrim(GetVehicleNumberPlateText(vehicleKey)) end local entityToDelete = (persistentData and persistentData.entity) or vehicleKey local vehicleCoords = GetEntityCoords(entityToDelete) local impound = GetImpoundOfType(vehicleType, vehicleCoords) if not impound then Debug("No impound for type:", vehicleType) return end -- Check if it is a job vehicle with its own garage local jobCheck = MySQL.Sync.fetchAll( string.format("SELECT * FROM %s WHERE plate = ? AND jobGarage != ''", garageTable), { plate } ) local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ?", garageTable, storedColumn ) if DoesEntityExist(entityToDelete) then DeleteEntity(entityToDelete) end if jobCheck[1] then local jobGarage = jobCheck[1].jobGarage MySQL.Sync.execute(updateQuery, { jobGarage, State.stored, plate }) Notification(sourcePlayer, i18n.t("vehicle_impounded", { garage = jobGarage }), "success") else MySQL.Sync.execute(updateQuery, { impound, State.impounded, plate }) -- QB-Core state update if Config.Framework == "qb" then Debug("Vehicle goes to state 2 (IMPOUND)") MySQL.update( "UPDATE player_vehicles SET state = ?, depotprice = ? WHERE plate = ?", { 2, Config.ImpoundPrice, plate } ) end if persistentData then PersistentVehicles[vehicleKey] = nil end Notification(sourcePlayer, i18n.t("vehicle_impounded", { garage = impound }), "success") end end exports("deleteVehicle", DeleteNearbyVehicle) RegisterCommand("mdv", DeleteNearbyVehicle) RegisterCommand("mdvall", function(sourcePlayer) if not PlayerIsAdmin(sourcePlayer) then return end local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ?", garageTable, storedColumn ) for key, vehicleData in pairs(PersistentVehicles) do local driverPed = GetPedInVehicleSeat(vehicleData.entity, -1) if driverPed == 0 then local impound = GetImpoundOfType(vehicleData.type, vehicleData.vehicle.coords) MySQL.Sync.execute(updateQuery, { impound, State.impounded, vehicleData.vehicle.plate }) if DoesEntityExist(vehicleData.entity) then DeleteEntity(vehicleData.entity) end PersistentVehicles[key] = nil if Config.Framework == "qb" then Debug("Vehicle goes to state 2 (IMPOUND)") MySQL.update( "UPDATE player_vehicles SET state = ?, depotprice = ? WHERE plate = ?", { 2, Config.ImpoundPrice, vehicleData.vehicle.plate } ) end end end Notification(sourcePlayer, i18n.t("all_vehicles_impounded"), "success") end, false) -- ────────────────────────────────────────────── -- impound (net event) -- ────────────────────────────────────────────── RegisterNetEvent("advancedgarages:impound") AddEventHandler("advancedgarages:impound", function(vehicleKey, impoundData) local playerSource = source local jobName = GetJobName(playerSource) if not IsJobAllowed(jobName, "impound") then Debug("Player is not allowed to impound", jobName) return end impoundData.time = os.time() + impoundData.time local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ?, impound_data = ? WHERE plate = ?", garageTable, storedColumn ) local entityToImpound = vehicleKey if PersistentVehicles[vehicleKey] then entityToImpound = PersistentVehicles[vehicleKey].entity end if not DoesEntityExist(entityToImpound) then Debug("Vehicle not spawned impound") return end Debug("Impounding vehicle", impoundData.selectedGarage) local plate = MathTrim(GetVehicleNumberPlateText(entityToImpound)) MySQL.Sync.execute(updateQuery, { impoundData.selectedGarage, State.impounded, json.encode(impoundData), plate, }) if DoesEntityExist(entityToImpound) then DeleteEntity(entityToImpound) end if PersistentVehicles[vehicleKey] then PersistentVehicles[vehicleKey] = nil end SendWebhook( Webhooks.impound_vehicle, i18n.t("log.impound_vehicle.title"), 7393279, i18n.t("log.impound_vehicle.description", { player = GetPlayerName(playerSource), job = jobName, plate = plate, garage = impoundData.selectedGarage, }) ) if Config.Framework == "qb" then Debug("Vehicle goes to state 2 (IMPOUND)") MySQL.update( "UPDATE player_vehicles SET state = ?, depotprice = ? WHERE plate = ?", { 2, impoundData.price, plate } ) end Notification(playerSource, i18n.t("vehicle_impounded", { garage = impoundData.selectedGarage }), "success") end) -- ────────────────────────────────────────────── -- impound export (by plate) -- ────────────────────────────────────────────── function ImpoundVehicleByPlate(plate) plate = MathTrim(plate) local vehicleKey = GetVehicleFromPlate(plate) if not vehicleKey then return Error("Vehicle not spawned, plate:", plate) end local vehicleData = PersistentVehicles[vehicleKey] local impound = GetImpoundOfType(vehicleData.type, vehicleData.vehicle.coords) local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ?", garageTable, storedColumn ) MySQL.Sync.execute(updateQuery, { impound, State.impounded, vehicleData.vehicle.plate }) local entity = vehicleData.entity if DoesEntityExist(entity) then DeleteEntity(entity) end PersistentVehicles[vehicleKey] = nil end exports("impound", ImpoundVehicleByPlate) -- Server event alias RegisterServerEvent("advancedgarages:server:impoundVehicle") AddEventHandler("advancedgarages:server:impoundVehicle", function(plate, impoundData) local playerSource = source plate = MathTrim(plate) local vehicleKey = GetVehicleFromPlate(plate) if not vehicleKey then return Error("Vehicle not spawned, plate:", plate) end local vehicleData = PersistentVehicles[vehicleKey] local impound = GetImpoundOfType(vehicleData.type, vehicleData.vehicle.coords) local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ?", garageTable, storedColumn ) MySQL.Sync.execute(updateQuery, { impound, State.impounded, vehicleData.vehicle.plate }) local entity = vehicleData.entity if DoesEntityExist(entity) then DeleteEntity(entity) end PersistentVehicles[vehicleKey] = nil Notification(playerSource, i18n.t("vehicle_impounded", { garage = impound }), "success") end) -- ────────────────────────────────────────────── -- SaveAllPersistentVehicles (batch SQL update) -- ────────────────────────────────────────────── function SaveAllPersistentVehicles() if type(PersistentVehicles) ~= "table" or next(PersistentVehicles) == nil then return end local rowTemplate = "SELECT %s AS id, '%s' AS new_data" local queryTemplate = [[ UPDATE %s u JOIN ( %s ) a ON u.id = a.id SET `%s` = new_data ]] local unionParts = {} for _, vehicleData in pairs(PersistentVehicles) do local part = string.format(rowTemplate, vehicleData.id, json.encode(vehicleData.vehicle)) table.insert(unionParts, part) end local unionSQL = table.concat(unionParts, "\n\nUNION ") local fullQuery = string.format(queryTemplate, garageTable, unionSQL, garagePropsColumn) MySQL.Sync.execute(fullQuery) end -- ────────────────────────────────────────────── -- Persistent threads (save + tick) -- ────────────────────────────────────────────── if Config.PersistentVehicles then -- Save loop every 12s CreateThread(function() while true do SaveAllPersistentVehicles() Wait(12000) end end) -- Property / respawn tick every 2s CreateThread(function() while true do Wait(2000) TickPersistentVehicles() end end) end