2026-04-14 17:41:39 +02:00

905 lines
35 KiB
Lua
Raw 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.

-- ============================================================
-- 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