-- ============================================================ -- main.lua – Core server logic -- ============================================================ Players = {} PlayerDefaultRoutings = {} CachedPlayerNames = {} JobGarageUsers = {} function IsJobAllowed(jobName, action) if not jobName then return false end if not Config.AllowedJobs then return false end for _, allowedJob in ipairs(Config.AllowedJobs) do if allowedJob == jobName then return true end end return false end print("^2[GARAGES] Initializing server cache tables...^7") local version = "5.1.2-FINAL" print("^2[GARAGES] Server version " .. version .. " loaded!^7") -- Wait for db module to be available while not db do Wait(1000) Debug("Waiting for db", "your db") end -- [DB AUTO-FIX] Ensure necessary columns exist for the menu buttons to work CreateThread(function() Wait(5000) -- Wait for MySQL to be fully ready print("^2[GARAGES] Checking database structure for tags and favorites...^7") -- Ensure 'tag' column exists MySQL.query("ALTER TABLE owned_vehicles ADD COLUMN IF NOT EXISTS tag VARCHAR(255) DEFAULT NULL") -- Ensure 'favorite' column exists MySQL.query("ALTER TABLE owned_vehicles ADD COLUMN IF NOT EXISTS favorite TINYINT(1) DEFAULT 0") print("^2[GARAGES] Database structure is now ready!^7") end) -- ────────────────────────────────────────────── -- GetImpoundOfType -- Returns the name of the nearest impound garage -- matching the given vehicle type (and optional coords). -- ────────────────────────────────────────────── local function ExtractCoordsFromGarageData(garageData) if not garageData then return nil end local function toVec3(obj) local x = tonumber(obj.x) local y = tonumber(obj.y) local z = tonumber(obj.z) if x and y and z then return vec3(x, y, z) end end if garageData.x then return toVec3(garageData) end if garageData.menuCoords and garageData.menuCoords.x then return toVec3(garageData.menuCoords) end if garageData.spawnCoords and garageData.spawnCoords.x then return toVec3(garageData.spawnCoords) end return nil end function GetImpoundOfType(vehicleType, vehicleCoords) if not vehicleCoords then -- No coords: return the first matching impound for garageName, garageData in pairs(Config.Garages) do if garageData.type == vehicleType and garageData.isImpound then return garageName end end return false end -- Collect all impounds of that type with valid coords local candidates = {} for garageName, garageData in pairs(Config.Garages) do if garageData.type == vehicleType and garageData.isImpound then local coords = ExtractCoordsFromGarageData(garageData.coords) if coords then table.insert(candidates, { name = garageName, coords = coords }) end end end if #candidates == 0 then return false end -- Sort by distance and return the nearest local refCoords = vehicleCoords table.sort(candidates, function(a, b) return #(a.coords - refCoords) < #(b.coords - refCoords) end) return candidates[1] and candidates[1].name or false end -- ────────────────────────────────────────────── -- Load garages from DB into Config -- ────────────────────────────────────────────── local dbGarages = db.getGarages() for _, garageData in pairs(dbGarages) do Config.Garages[garageData.name] = garageData end -- ────────────────────────────────────────────── -- Migration command -- ────────────────────────────────────────────── RegisterCommand("migratev5", function(sourcePlayer) if sourcePlayer ~= 0 then return print("This command can only be used by the server") end db.checkVehicleGarages() print("Migrate v5") end) -- ────────────────────────────────────────────── -- Load furniture data -- ────────────────────────────────────────────── Config.Furniture = db.getMergedFurnitureData() -- ────────────────────────────────────────────── -- Notification helper -- ────────────────────────────────────────────── function Notification(playerSource, message, notifType) TriggerClientEvent("garages:notification", playerSource, message, notifType) end -- ────────────────────────────────────────────── -- ClearGarage – moves all vehicles in a garage to impound -- ────────────────────────────────────────────── function ClearGarage(garageId, playerIdentifier) local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE garage = ? AND %s = ?", garageTable, storedColumn, garageIdentifierColumn ) local selectQuery = string.format( "SELECT type, %s FROM %s WHERE garage = ? AND %s = ? LIMIT 1", garagePropsColumn, garageTable, garageIdentifierColumn ) local vehicles = MySQL.Sync.fetchAll(selectQuery, { garageId, playerIdentifier }) for _, row in pairs(vehicles) do local rawProps = row[garagePropsColumn] local props = json.decode(rawProps) local vehicleCoords = (props and props.coords) or nil local impound = GetImpoundOfType(row.type, vehicleCoords) if not impound then Debug("Type:", row.type, "not found impound in [sellGarage], please create that type impound.") return false end MySQL.Sync.execute(updateQuery, { impound, State.impounded, garageId, playerIdentifier }) end ClearDecorations(garageId) MySQL.prepare("DELETE FROM qs_garage_decorations WHERE insideId = ?", { garageId }) end -- ────────────────────────────────────────────── -- Player connected -- ────────────────────────────────────────────── RegisterNetEvent("garages:playerConnected") AddEventHandler("garages:playerConnected", function() local playerSource = source table.insert(Players, playerSource) UpdatePlayerCache(playerSource) TriggerClientEvent("garages:setGarages", playerSource, Config.Garages) TriggerClientEvent("garages:syncFurnitureData", playerSource, Config.Furniture) local identifier = GetPlayerIdentifier(playerSource) if identifier then InitInsideShellGarage(playerSource) end Debug("Player connected", playerSource) end) -- ────────────────────────────────────────────── -- ox-inventory stash registration -- ────────────────────────────────────────────── RegisterNetEvent("garages:ox:registerStash") AddEventHandler("garages:ox:registerStash", function(stashId, slots, maxWeight) exports.ox_inventory:RegisterStash(stashId, "stash" .. stashId, slots, maxWeight, false) end) -- ────────────────────────────────────────────── -- qb-inventory open stash -- ────────────────────────────────────────────── RegisterNetEvent("garages:openStash") AddEventHandler("garages:openStash", function(stashId, stashData) local playerSource = source exports["qb-inventory"]:OpenInventory(playerSource, stashId, stashData) end) -- ────────────────────────────────────────────── -- GetBalance callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:getBalance", function(sourcePlayer) return GetAccountMoney(sourcePlayer, Config.MoneyType) end) -- ────────────────────────────────────────────── -- Sell garage -- ────────────────────────────────────────────── local function SellGarage(garageId) local playerSource = source local playerIdentifier = GetPlayerIdentifier(playerSource) local garageData = Config.Garages[garageId] if not garageData then return Notification(playerSource, i18n.t("garage_not_found"), "error") end local sellPrice = garageData.price * Config.GarageSellTax ClearGarage(garageId, playerIdentifier) MySQL.Sync.execute( "UPDATE player_garages SET owner = ? WHERE name = ? AND owner = ? LIMIT 1", { "", garageId, playerIdentifier } ) AddAccountMoney(playerSource, Config.MoneyType, sellPrice) Notification(playerSource, i18n.t("sell_garage", { price = sellPrice }), "error") Config.Garages[garageId].owner = nil TriggerClientEvent("garages:updateGaragePartial", -1, { name = garageId, owner = false }) end RegisterNetEvent("advancedgarages:sellGarage") AddEventHandler("advancedgarages:sellGarage", SellGarage) -- ────────────────────────────────────────────── -- Buy garage -- ────────────────────────────────────────────── local function BuyGarage(garageId, price) local playerSource = source local playerIdentifier = GetPlayerIdentifier(playerSource) local balance = GetAccountMoney(playerSource, Config.MoneyType) if price > balance then return Notification(playerSource, i18n.t("no_money", { price = price }), "error") end RemoveAccountMoney(playerSource, Config.MoneyType, price) local existingRow = MySQL.Sync.fetchAll("SELECT 1 FROM player_garages WHERE name = ?", { garageId }) if not existingRow[1] then local garageData = Config.Garages[garageId] if not garageData then return Debug(garageId .. " not found in Config...") end MySQL.Sync.execute( "INSERT INTO player_garages (owner, name, price, coords, shell, type) VALUES (?, ?, ?, ?, ?, ?)", { playerIdentifier, garageId, garageData.price, json.encode(garageData.coords), json.encode(garageData.shell), garageData.type } ) else MySQL.Sync.execute( "UPDATE player_garages SET owner = ? WHERE name = ?", { playerIdentifier, garageId } ) end Config.Garages[garageId].owner = playerIdentifier Notification(playerSource, i18n.t("garage_purchase", { price = price }), "error") TriggerClientEvent("garages:updateGaragePartial", -1, { name = garageId, owner = playerIdentifier }) end RegisterNetEvent("advancedgarages:buyGarage") AddEventHandler("advancedgarages:buyGarage", BuyGarage) -- ────────────────────────────────────────────── -- Startup: fix vehicles with NULL garage -- ────────────────────────────────────────────── CreateThread(function() Wait(1250) local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE garage IS NULL", garageTable, storedColumn ) local selectQuery = string.format( "SELECT type, %s FROM %s WHERE garage IS NULL", garagePropsColumn, garageTable, garageIdentifierColumn ) local nullGarageVehicles = MySQL.Sync.fetchAll(selectQuery) for _, row in pairs(nullGarageVehicles) do local rawProps = row[garagePropsColumn] local props = json.decode(rawProps) local vehicleCoords = (props and props.coords) or nil local impound = GetImpoundOfType(row.type, vehicleCoords) if not impound then Debug("Type:", row.type, "not found impound in [First Thread], please create that type impound") return end MySQL.Sync.execute(updateQuery, { impound, State.impounded }) Debug("Some vehicles without predetermined garage were sent to the nearest Impound") end end) -- ────────────────────────────────────────────── -- Key holder callbacks and events -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:getGarageKeyHolders", function(sourcePlayer, garageId) local garageData = Config.Garages[garageId] if not garageData then return false end local holders = garageData.holders if not holders then return false end local result = {} for _, identifier in pairs(holders) do local firstName, lastName = GetUserData(identifier) table.insert(result, { firstname = firstName, lastname = lastName, identifier = identifier }) end return result end) -- Track which player is currently in a job garage local jobGarageUsers = JobGarageUsers lib.callback.register("advancedgarages:isGarageAvailable", function(sourcePlayer, garageId) return jobGarageUsers[garageId] == nil end) RegisterNetEvent("advancedgarages:setInJobGarage") AddEventHandler("advancedgarages:setInJobGarage", function(garageId, isEntering) local playerSource = source if not garageId then return end jobGarageUsers[garageId] = (isEntering and playerSource) or nil end) -- Take key event RegisterNetEvent("advancedgarages:takeKey") AddEventHandler("advancedgarages:takeKey", function(data) local playerSource = source local targetId = data.id local garageId = data.garage local garageData = Config.Garages[garageId] if not garageData then return Notification(playerSource, i18n.t("garage_not_found"), "error") end local holders = garageData.holders if not holders then return Notification(playerSource, i18n.t("garage_no_keyholders"), "info") end for idx, identifier in pairs(holders) do if identifier == targetId then table.remove(garageData.holders, idx) MySQL.update.await( "UPDATE player_garages SET holders = ? WHERE name = ?", { json.encode(garageData.holders), garageId } ) TriggerClientEvent("garages:updateGaragePartial", -1, { name = garageId, holders = garageData.holders }) Notification(playerSource, i18n.t("garage_taked_keys"), "success") return end end end) -- Give key event RegisterNetEvent("advancedgarages:giveKey") AddEventHandler("advancedgarages:giveKey", function(data) local playerSource = source local targetId = data.id local garageId = data.garage local targetIdent = GetPlayerIdentifier(targetId) local garageHolders = Config.Garages[garageId].holders if not targetIdent then return Notification(playerSource, i18n.t("management.no_player"), "error") end local targetFirstName, targetLastName = GetUserData(targetIdent) if garageHolders then for _, identifier in pairs(garageHolders) do if identifier == targetIdent then Notification(playerSource, i18n.t("keyholders.already_keys"), "info") return end end table.insert(garageHolders, targetIdent) else garageHolders = { targetIdent } end MySQL.update.await( "UPDATE player_garages SET holders = ? WHERE name = ?", { json.encode(garageHolders), garageId } ) TriggerClientEvent("garages:updateGaragePartial", -1, { name = garageId, holders = garageHolders }) Notification(playerSource, i18n.t("keyholders.gave_keys", { garage = garageId, player = targetFirstName .. " " .. targetLastName }), "success") Notification(targetId, i18n.t("keyholders.received_keys", { garage = garageId }), "success") end) -- ────────────────────────────────────────────── -- Shell vehicle list callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:GetShellVehicleList", function(sourcePlayer, garageId) local query local args local identifier = GetPlayerIdentifier(sourcePlayer) if Config.GlobalGarageSync then query = "SELECT * FROM " .. garageTable .. " WHERE " .. garageIdentifierColumn .. " = ?" args = { identifier } else query = string.format("SELECT * FROM %s WHERE garage = ?", garageTable) if Config.GarageSync then args = { garageId } else query = query .. string.format(" AND %s = ?", garageIdentifierColumn) args = { garageId, identifier } end end local rows = MySQL.Sync.fetchAll(query, args) if not rows[1] then return false end local vehicles = {} for _, row in pairs(rows) do if Config.Framework == "qb" then row.vehicle = row.mods row.owner = row.citizenid end local props = json.decode(row.vehicle) if props and props.model then vehicles[#vehicles + 1] = row else Debug("Vehicle with plate:", row.plate, "has not `vehicle` data. We are skipping it. But this problem is not related to the script.") end end return vehicles end) -- ────────────────────────────────────────────── -- GetPlayerSqlName callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:GetPlayerSqlName", function(sourcePlayer, targetId) local identifier = GetPlayerIdentifier(targetId) local firstName, lastName = GetUserData(identifier) return { firstName = firstName, lastName = lastName } end) -- ────────────────────────────────────────────── -- getGarages callback (player garages list) -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:getGarages", function(sourcePlayer) local identifier = GetPlayerIdentifier(sourcePlayer) local query = string.format([[ SELECT player_garages.*, COUNT(%s.id) as vehicleCount FROM player_garages LEFT JOIN %s ON player_garages.name = %s.garage WHERE player_garages.owner = ? GROUP BY player_garages.name ]], garageTable, garageTable, garageTable) local garages = MySQL.Sync.fetchAll(query, { identifier }) for _, garage in pairs(garages) do garage.coords = json.decode(garage.coords).menuCoords if not garage.interior_type then garage.interior_type = "ipl" end end return garages end) -- ────────────────────────────────────────────── -- GetVehicleFromPlate callback (DB lookup) -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:GetVehicleFromPlate", function(sourcePlayer, plate) local identifier = GetPlayerIdentifier(sourcePlayer) if not plate then Error("advancedgarages:GetVehicleFromPlate ::: Plate not found", plate) return false end local query = string.format( "SELECT * FROM %s WHERE plate = ? AND %s = ? LIMIT 1", garageTable, garageIdentifierColumn ) local result = MySQL.Sync.fetchAll(query, { plate, identifier }) if Config.Framework == "qb" and result[1] then result[1].vehicle = result[1].mods result[1].owner = result[1].citizenid end return result[1] end) -- ────────────────────────────────────────────── -- getGarageSlots callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:getGarageSlots", function(sourcePlayer, garageId) local identifier = GetPlayerIdentifier(sourcePlayer) local query = string.format( "SELECT COUNT(*) as count FROM %s WHERE garage = ? AND %s = ?", garageTable, garageIdentifierColumn ) local result = MySQL.Sync.fetchAll(query, { garageId, identifier }) return result[1].count end) -- ────────────────────────────────────────────── -- Money callbacks -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:existMoney", function(sourcePlayer, amount) local balance = GetAccountMoney(sourcePlayer, Config.MoneyType) return balance >= tonumber(amount) end) lib.callback.register("advancedgarages:removeMoney", function(sourcePlayer, amount) return RemoveAccountMoney(sourcePlayer, Config.MoneyType, tonumber(amount)) end) -- ────────────────────────────────────────────── -- Startup: send "garage 1" (invalid) vehicles to impound -- ────────────────────────────────────────────── CreateThread(function() Wait(1250) local selectQuery = string.format("SELECT * FROM %s WHERE garage = '1'", garageTable) local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ?", garageTable, storedColumn ) local badVehicles = MySQL.Sync.fetchAll(selectQuery) for _, row in pairs(badVehicles) do if row.type == "car" or row.type == "automobile" then row.type = "vehicle" end local rawProps = row[garagePropsColumn] local props = json.decode(rawProps) local vehicleCoords = (props and props.coords) or nil local impound = GetImpoundOfType(row.type, vehicleCoords) if not impound then Debug("No impound for type:", row.type, "in [sentToImpound] debug") else MySQL.Sync.execute(updateQuery, { impound, State.impounded, row.plate }) Debug("Vehicle with plate:", row.plate, 'moved to impound because garage "1" does not exist') end end end) RegisterNetEvent("advancedgarages:server:sentToImpound") AddEventHandler("advancedgarages:server:sentToImpound", function() local selectQuery = string.format("SELECT * FROM %s WHERE garage = '1'", garageTable) local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ?", garageTable, storedColumn ) local badVehicles = MySQL.Sync.fetchAll(selectQuery) for _, row in pairs(badVehicles) do if row.type == "car" or row.type == "automobile" then row.type = "vehicle" end local rawProps = row[garagePropsColumn] local props = json.decode(rawProps) local vehicleCoords = (props and props.coords) or nil local impound = GetImpoundOfType(row.type, vehicleCoords) if not impound then Warning("No impound for type:", row.type, "in [sentToImpound] debug") else MySQL.Sync.execute(updateQuery, { impound, State.impounded, row.plate }) Debug("Vehicle with plate:", row.plate, 'moved to impound because garage "1" does not exist') end end end) -- ────────────────────────────────────────────── -- getImpoundGarages export -- ────────────────────────────────────────────── local function GetImpoundGarages() local result = {} for garageName, garageData in pairs(Config.Garages) do if garageData.isImpound then result[garageName] = garageData end end return result end exports("getImpoundGarages", GetImpoundGarages) -- ────────────────────────────────────────────── -- getGarageData callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:getGarageData", function(sourcePlayer, garageId) local identifier = GetPlayerIdentifier(sourcePlayer) print("[GARAGES] Debug: Lecture du garage '" .. tostring(garageId) .. "' pour l'identifier: " .. tostring(identifier)) local vehicles if Config.GlobalGarageSync then local query = "SELECT * FROM " .. garageTable .. " WHERE " .. garageIdentifierColumn .. " = ?" vehicles = MySQL.Sync.fetchAll(query, { identifier }) else vehicles = MySQL.Sync.fetchAll(Query.SELECT_PLAYER_VEHICLES, { garageId, identifier }) end print("[GARAGES] Debug: " .. #vehicles .. " vehicules trouves !") local result = {} for _, row in pairs(vehicles) do if Config.Framework == "qb" then row.vehicle = row.mods row.owner = row.citizenid end if row.impound_data and row.impound_data ~= "" then row.impound_data = json.decode(row.impound_data) end local props = json.decode(row.vehicle) if props and props.model then -- [AUTO-FIX] On force l'interface à savoir qu'on est le proprio row.favorite = row.favorite or 0 row.tag = row.tag or "" row.isOwner = true -- Force le dégrisage des boutons row.owner = identifier -- Assure la correspondance d'ID result[#result + 1] = row else Debug("Vehicle with plate:", row.plate, "has no `vehicle` data. Skipping.") end end return result end) -- ────────────────────────────────────────────── -- isVehicleOwned callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:isVehicleOwned", function(sourcePlayer, plate) local identifier = GetPlayerIdentifier(sourcePlayer) local query = string.format( "SELECT 1 FROM %s WHERE plate = ? AND %s = ? AND jobVehicle = ? LIMIT 1", garageTable, garageIdentifierColumn ) local result = MySQL.Sync.fetchAll(query, { plate, identifier, "" }) return result[1] ~= nil end) -- ────────────────────────────────────────────── -- transferVehicle callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:transferVehicle", function(sourcePlayer, targetGarage, plate) local identifier = GetPlayerIdentifier(sourcePlayer) local query = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ? AND %s = ?", garageTable, storedColumn, garageIdentifierColumn ) local affected = MySQL.Sync.execute(query, { targetGarage, State.stored, plate, identifier }) RemoveAccountMoney(sourcePlayer, Config.MoneyType, Config.TransferGaragePrice) return 1 == affected end) -- ────────────────────────────────────────────── -- takeVehicleFromOut callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:takeVehicleFromOut", function(sourcePlayer, targetGarage, plate) local identifier = GetPlayerIdentifier(sourcePlayer) local query = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE `plate` = ? AND `%s` = ?", garageTable, storedColumn, garageIdentifierColumn ) local affected = MySQL.Sync.execute(query, { targetGarage, State.stored, plate, identifier }) RemoveAccountMoney(sourcePlayer, Config.MoneyType, Config.ImpoundPrice) local vehicleKey = GetVehicleFromPlate(plate) if vehicleKey then local entity = PersistentVehicles[vehicleKey] and PersistentVehicles[vehicleKey].entity if entity and DoesEntityExist(entity) then DeleteEntity(entity) end PersistentVehicles[vehicleKey] = nil end return 1 == affected end) -- ────────────────────────────────────────────── -- getOutVehicles callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:getOutVehicles", function(sourcePlayer, garageId) local identifier = GetPlayerIdentifier(sourcePlayer) local garageData = Config.Garages[garageId] if not garageData then Error("advancedgarages:getOutVehicles ::: Garage not found:", garageId) return {} end local typeFilter = "" if garageData.type == "vehicle" then typeFilter = 'AND type IN("vehicles", "car", "pdm", "vehicle", "automobile")' elseif garageData.type == "plane" then typeFilter = 'AND type IN("airplane", "air", "helicopter")' elseif garageData.type == "boat" then typeFilter = 'AND type IN("water")' end local query = string.format( "SELECT * FROM %s WHERE garage != ? AND %s = ? AND impound_data = '' AND jobVehicle = '' %s", garageTable, garageIdentifierColumn, typeFilter ) local vehicles = MySQL.Sync.fetchAll(query, { garageId, identifier }) for _, row in pairs(vehicles) do if Config.Framework == "qb" then row.vehicle = row.mods row.owner = row.citizenid end end Debug("advancedgarages:getOutVehicles", #vehicles) return vehicles end) -- ────────────────────────────────────────────── -- transferVehicleToPlayer callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:transferVehicleToPlayer", function(sourcePlayer, targetId, plate) targetId = tonumber(targetId) local targetIdent = GetPlayerIdentifier(targetId) if not targetIdent then Error("advancedgarages:transferVehicleToPlayer ::: Target identifier not found for playerId:", targetId) Notification(sourcePlayer, i18n.t("player_not_found"), "error") return end local propsQuery = string.format( "SELECT type, %s FROM %s WHERE plate = ? LIMIT 1", garagePropsColumn, garageTable ) local propsResult = MySQL.Sync.fetchAll(propsQuery, { plate }) if not propsResult[1] then return false end -- Normalize vehicle type local vehicleType = propsResult[1].type local typeMap = { car = "vehicle", automobile = "vehicle", pdm = "vehicle", airplane = "plane", air = "plane", helicopter = "plane", heli = "plane", water = "boat" } vehicleType = typeMap[vehicleType] or vehicleType propsResult[1].type = vehicleType local rawProps = propsResult[1][garagePropsColumn] local props = json.decode(rawProps) local vehicleCoords = (props and props.coords) or nil -- Check transfer blacklist if Config.TransferBlacklist and #Config.TransferBlacklist > 0 then local model = props and props.model for _, blacklistedModel in pairs(Config.TransferBlacklist) do if joaat(blacklistedModel) == model then return Notification(sourcePlayer, i18n.t("transfer_blacklisted"), "error") end end end local impound = GetImpoundOfType(vehicleType, vehicleCoords) if not impound then Debug("Type:", vehicleType, "not found impound in [transferVehicleToPlayer], please create that type impound") return false end local sourceIdent = GetPlayerIdentifier(sourcePlayer) local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ?, `%s` = ? WHERE plate = ? AND %s = ?", garageTable, storedColumn, garageIdentifierColumn, garageIdentifierColumn ) local affected = MySQL.Sync.execute(updateQuery, { impound, State.impounded, targetIdent, plate, sourceIdent }) RemoveAccountMoney(sourcePlayer, Config.MoneyType, Config.TransferGaragePrice) Notification(sourcePlayer, i18n.t("transfer_success_to", { garage = impound }), "info") Notification(targetId, i18n.t("transfer_success", { garage = impound }), "info") return 1 == affected end) -- ────────────────────────────────────────────── -- RoutePlayer / RoutePlayerDefault events -- ────────────────────────────────────────────── RegisterNetEvent("advancedgarages:RoutePlayer") AddEventHandler("advancedgarages:RoutePlayer", function(data) local playerSource = source local currentBucket = GetPlayerRoutingBucket(playerSource) PlayerDefaultRoutings[playerSource] = currentBucket SetPlayerRoutingBucket(playerSource, math.random(1, 100000)) end) RegisterNetEvent("advancedgarages:RoutePlayerDefault") AddEventHandler("advancedgarages:RoutePlayerDefault", function(garageId) local playerSource = source SetPlayerRoutingBucket(playerSource, 0) PlayerDefaultRoutings[playerSource] = nil local identifier = GetPlayerIdentifier(playerSource) LeavelShellGarage(garageId) MySQL.Sync.execute( string.format("UPDATE %s SET shell_garage = NULL WHERE %s = ?", userTable, identifierColumn), { identifier } ) end) -- ────────────────────────────────────────────── -- GetUserData – get player firstname/lastname -- ────────────────────────────────────────────── function GetUserData(identifier) if Config.Framework == "qb" then local player = GetPlayerFromIdentifier(identifier) if not player then return false end local charInfo = player.PlayerData.charinfo return charInfo.firstname, charInfo.lastname end local query = string.format("SELECT firstname, lastname FROM %s WHERE %s = ?", userTable, identifierColumn) local result = MySQL.Sync.fetchAll(query, { identifier }) if not result[1] then return false end local firstName = result[1].firstname or "Player" local lastName = result[1].lastname or "Player" return firstName, lastName end -- ────────────────────────────────────────────── -- savePlayer -- ────────────────────────────────────────────── function UpdatePlayerCache(playerSource) local identifier = GetPlayerIdentifier(playerSource) if not identifier then return Wait(150) end local firstName, lastName = GetUserData(identifier) CachedPlayerNames[playerSource] = firstName .. " " .. lastName end -- ────────────────────────────────────────────── -- Initial player name cache thread -- ────────────────────────────────────────────── CreateThread(function() for _, playerSource in ipairs(Players) do UpdatePlayerCache(playerSource) end end) -- ────────────────────────────────────────────── -- playerDropped cleanup -- ────────────────────────────────────────────── AddEventHandler("playerDropped", function(reason) local playerSource = source -- Remove from job garage users for garageId, user in pairs(JobGarageUsers or {}) do if user == playerSource then JobGarageUsers[garageId] = nil end end -- Remove from Players list for idx, player in pairs(Players) do if player == playerSource then table.remove(Players, idx) end end -- Remove from name cache if CachedPlayerNames and CachedPlayerNames[playerSource] then CachedPlayerNames[playerSource] = nil end end) -- ────────────────────────────────────────────── -- GetPlayersName callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:GetPlayersName", function(sourcePlayer) return CachedPlayerNames end) -- ────────────────────────────────────────────── -- getJobVehicles callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:getJobVehicles", function(sourcePlayer, garageId, jobName) if not garageId then return false end local query = string.format("SELECT * FROM %s WHERE garage = ? AND jobVehicle = ?", garageTable) local results = MySQL.Sync.fetchAll(query, { garageId, jobName }) for _, row in pairs(results) do if Config.Framework == "qb" then row.vehicle = row.mods row.owner = row.citizenid end row.vehicle = json.decode(row.vehicle) end return results end) -- ────────────────────────────────────────────── -- addJobVehicle event -- ────────────────────────────────────────────── RegisterNetEvent("advancedgarages:addJobVehicle") AddEventHandler("advancedgarages:addJobVehicle", function(jobName, plate, garageData) local playerSource = source if not jobName or not plate or not garageData then return end if GetJobName(playerSource) ~= jobName then return end local identifier = GetPlayerIdentifier(playerSource) local selectQuery = string.format( "SELECT * FROM %s WHERE plate = ? AND %s = ? LIMIT 1", garageTable, garageIdentifierColumn ) local existing = MySQL.Sync.fetchAll(selectQuery, { plate, identifier }) if not existing[1] then return end local updateQuery = string.format( "UPDATE %s SET jobVehicle = ?, garage = ? WHERE plate = ? AND %s = ?", garageTable, garageIdentifierColumn ) MySQL.Sync.execute(updateQuery, { jobName, garageData.name, plate, identifier }) 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) -- ────────────────────────────────────────────── -- deleteJobVehicle event -- ────────────────────────────────────────────── RegisterNetEvent("advancedgarages:deleteJobVehicle") AddEventHandler("advancedgarages:deleteJobVehicle", function(data) local playerSource = source local plate = data.plate local garageId = data.garage Debug("Work vehicle removed, info:", json.encode(plate), json.encode(garageId)) if not plate or not garageId then return end local identifier = GetPlayerIdentifier(playerSource) local selectQuery = string.format( "SELECT * FROM %s WHERE plate = ? AND %s = ? LIMIT 1", garageTable, garageIdentifierColumn ) local existing = MySQL.Sync.fetchAll(selectQuery, { plate, identifier }) if not existing[1] then return end local rawProps = existing[1][garagePropsColumn] local props = json.decode(rawProps) local vehicleCoords = (props and props.coords) or nil local impound = GetImpoundOfType(existing[1].type, vehicleCoords) if not impound then Debug("Type:", existing[1].type, "not found impound in [deleteJobVehicle], please create that type impound") return false end local updateQuery = string.format( "UPDATE %s SET jobVehicle = ?, garage = ? WHERE plate = ? AND %s = ?", garageTable, garageIdentifierColumn ) MySQL.Sync.execute(updateQuery, { "", impound, plate, identifier }) Notification(playerSource, i18n.t("vehicle_impounded", { garage = impound }), "info") end) -- ────────────────────────────────────────────── -- storeJobVehicle event -- ────────────────────────────────────────────── RegisterNetEvent("advancedgarages:storeJobVehicle") AddEventHandler("advancedgarages:storeJobVehicle", function(garageId, plate, vehicleProps) if not garageId or not plate then return end local query = string.format( "UPDATE %s SET `garage` = ?, `%s` = ?, `%s` = ?, `jobGarage` = '' WHERE plate = ?", garageTable, storedColumn, garagePropsColumn ) MySQL.Sync.execute(query, { garageId, State.stored, json.encode(vehicleProps), plate }) end) -- ────────────────────────────────────────────── -- isVehicleOwnedByJob callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:isVehicleOwnedByJob", function(sourcePlayer, plate, jobName) local query = string.format( "SELECT 1 FROM %s WHERE plate = ? AND jobVehicle = ? AND garage = 'OUT'", garageTable ) local result = MySQL.Sync.fetchAll(query, { plate, jobName }) return result[1] ~= nil end) -- ────────────────────────────────────────────── -- Startup: store all OUT job vehicles -- ────────────────────────────────────────────── CreateThread(function() local query = string.format( "UPDATE %s SET `garage` = %s.jobGarage, `%s` = ? WHERE garage = 'OUT' AND jobVehicle != ''", garageTable, garageTable, storedColumn ) MySQL.Sync.execute(query, { State.stored }) end) -- ────────────────────────────────────────────── -- takeoutJobVehicle event -- ────────────────────────────────────────────── RegisterNetEvent("advancedgarages:takeoutJobVehicle") AddEventHandler("advancedgarages:takeoutJobVehicle", function(plate) if not plate then return end local selectQuery = string.format("SELECT * FROM %s WHERE plate = ? LIMIT 1", garageTable) local existing = MySQL.Sync.fetchAll(selectQuery, { plate }) if not existing[1] then return end local originalGarage = existing[1].garage local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ?, `jobGarage` = ? WHERE plate = ?", garageTable, storedColumn ) MySQL.Sync.execute(updateQuery, { "OUT", State.out, originalGarage, plate }) end) -- ────────────────────────────────────────────── -- saveVehicle callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:saveVehicle", function(sourcePlayer, garageId, vehicleProps, deformations) if not deformations then deformations = {} end local plate = MathTrim(vehicleProps.plate) vehicleProps.deformations = deformations local query = string.format( "UPDATE %s SET `garage` = ?, `%s` = ?, `%s` = ? WHERE plate = ?", garageTable, storedColumn, garagePropsColumn ) MySQL.Sync.execute(query, { garageId, State.stored, json.encode(vehicleProps), plate }) local vehicleKey = GetVehicleFromPlate(plate) if not vehicleKey then return false end local qsInventoryState = GetResourceState("qs-inventory") if qsInventoryState and qsInventoryState:find("started") then exports["qs-inventory"]:UpdateQuestProgress(sourcePlayer, "deposit_vehicle_garage", 100) end SendWebhook( Webhooks.save_vehicle, i18n.t("log.save_vehicle.title"), 7393279, i18n.t("log.save_vehicle.description", { player = GetPlayerName(sourcePlayer), model = vehicleProps.model, plate = plate, garage = garageId, }) ) PersistentVehicles[vehicleKey] = nil return true end) -- ────────────────────────────────────────────── -- checkTimeout callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:checkTimeout", function(sourcePlayer) return false -- Pas de timeout par défaut pour débloquer le spawn end) -- ────────────────────────────────────────────── -- DriveVehicle callback (spawn from garage) -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:DriveVehicle", function(sourcePlayer, vehicleProps, spawnCoords, impoundFee) print("[GARAGES] Tentative de sortie: " .. tostring(vehicleProps.plate) .. " pour le joueur " .. tostring(sourcePlayer)) if SpawnTimeout then local elapsed = os.time() - SpawnTimeout if elapsed < 5 then Debug("SpawnTimeout:", elapsed, "< 5") return false end end SpawnTimeout = os.time() -- Mark vehicle as OUT in DB local updateQuery = string.format( "UPDATE %s SET `garage` = ?, `%s` = ?, `impound_data` = ?, `%s` = ? WHERE plate = ?", garageTable, storedColumn, garagePropsColumn ) vehicleProps.coords = spawnCoords local rowsChanged = MySQL.Sync.execute(updateQuery, { "OUT", State.out, "", json.encode(vehicleProps), vehicleProps.plate }) print("[GARAGES] Mise a jour SQL 'OUT' effectuee. Lignes affectees: " .. tostring(rowsChanged)) -- Deduct impound fee if any if impoundFee then local fee = tonumber(impoundFee) if fee then RemoveAccountMoney(sourcePlayer, Config.MoneyType, fee) end end -- Fetch fresh DB row local selectQuery = string.format("SELECT * FROM %s WHERE plate = ?", garageTable) local dbResult = MySQL.Sync.fetchAll(selectQuery, { vehicleProps.plate }) if not dbResult[1] then print("[GARAGES] ERREUR FATALE: Vehicule introuvable en base apres UPDATE ! Plaque: " .. tostring(vehicleProps.plate)) SpawnTimeout = nil Error("Vehicle not found in [DriveVehicle]. Plate:", vehicleProps.plate) return false end local row = dbResult[1] if Config.Framework == "qb" then row.vehicle = row.mods end local ownerIdent = row[garageIdentifierColumn] if not row then SpawnTimeout = nil Error("2: Vehicle not found in [DriveVehicle]. Plate:", vehicleProps.plate) return false end local props = row.vehicle and json.decode(row.vehicle) or {} if not props then SpawnTimeout = nil Error("There was a problem loading the props for said vehicle...") return false end local spawnedEntry = SpawnVehicle(row.id, ownerIdent, row.type, spawnCoords, props, sourcePlayer, true) if not spawnedEntry then print("[GARAGES] ERREUR : La voiture n'a pas pu etre creee physiquement (SpawnVehicle = nil). Type=" .. tostring(row.type)) SpawnTimeout = nil return false end print("[GARAGES] VICTOIRE ! La voiture est dehors !") SendWebhook( Webhooks.take_vehicle, i18n.t("log.take_vehicle.title"), 7393279, i18n.t("log.take_vehicle.description", { player = GetPlayerName(sourcePlayer), model = props.model, plate = props.plate, }) ) SpawnTimeout = nil return spawnedEntry end) -- ────────────────────────────────────────────── -- SetVehicleTag event -- ────────────────────────────────────────────── RegisterNetEvent("advancedgarages:SetVehicleTag") AddEventHandler("advancedgarages:SetVehicleTag", function(tag, plate) local playerSource = source local identifier = GetPlayerIdentifier(playerSource) if not identifier or not tag or not plate then return end local query = string.format("UPDATE %s SET tag = ? WHERE plate = ? AND %s = ?", garageTable, garageIdentifierColumn) MySQL.Async.execute(query, { tostring(tag), plate, identifier }, function(rowsAffected) if rowsAffected > 0 then Debug("Tag updated successfully for plate", plate) end end) end) RegisterNetEvent("advancedgarages:setVehicleFav") AddEventHandler("advancedgarages:setVehicleFav", function(isFavorite, plate) local playerSource = source local identifier = GetPlayerIdentifier(playerSource) if not identifier or not plate then return end -- Conversion booléen en int pour mysql local favValue = isFavorite and 1 or 0 local query = string.format("UPDATE %s SET favorite = ? WHERE plate = ? AND %s = ?", garageTable, garageIdentifierColumn) MySQL.Async.execute(query, { favValue, plate, identifier }, function(rowsAffected) if rowsAffected > 0 then Debug("Favorite status updated successfully for plate", plate) end end) end) -- ────────────────────────────────────────────── -- garagemenu command -- ────────────────────────────────────────────── RegisterCommand("garagemenu", function(sourcePlayer, args) TriggerClientEvent("advancedgarages:OpenGarageMenu", sourcePlayer) end, false) -- ────────────────────────────────────────────── -- getRecoveryVehicles callback -- ────────────────────────────────────────────── lib.callback.register("advancedgarages:getRecoveryVehicles", function(sourcePlayer) local identifier = GetPlayerIdentifier(sourcePlayer) local query = string.format( "SELECT * FROM %s WHERE garage = 'OUT' AND %s = ?", garageTable, garageIdentifierColumn ) return MySQL.Sync.fetchAll(query, { identifier }) end) -- ────────────────────────────────────────────── -- RecoveryVehicle event -- ────────────────────────────────────────────── RegisterNetEvent("advancedgarages:RecoveryVehicle") AddEventHandler("advancedgarages:RecoveryVehicle", function(vehicleData) local playerSource = source local plate = vehicleData.plate local vehicleType = vehicleData.type local typeMap = { car = "vehicle", automobile = "vehicle", pdm = "vehicle", airplane = "plane", air = "plane", helicopter = "plane", heli = "plane", water = "boat" } vehicleType = typeMap[vehicleType] or vehicleType local balance = GetAccountMoney(playerSource, Config.MoneyType) local recoveryPrice = Config.Recovery.price if balance < recoveryPrice then return Notification(playerSource, i18n.t("no_money", { price = recoveryPrice }), "error") end RemoveAccountMoney(playerSource, Config.MoneyType, recoveryPrice) local identifier = GetPlayerIdentifier(playerSource) local impound = GetImpoundOfType(vehicleType) local query = string.format( "UPDATE %s SET `garage` = ?, `%s` = ? WHERE plate = ? AND %s = ?", garageTable, storedColumn, garageIdentifierColumn ) MySQL.Sync.execute(query, { impound, State.impounded, plate, identifier }) SendWebhook( Webhooks.recovery_vehicle, i18n.t("log.recovery_vehicle.title"), 7393279, i18n.t("log.recovery_vehicle.description", { player = GetPlayerName(playerSource), model = plate, garage = impound, price = recoveryPrice, }) ) -- Remove from persistent vehicles if spawned for key, persistentData in pairs(PersistentVehicles) do if persistentData.vehicle.plate == plate then if DoesEntityExist(persistentData.entity) then DeleteEntity(persistentData.entity) end PersistentVehicles[key] = nil Debug("Vehicle with plate:", plate, ", was removed from spawnedVehicles list!") break end end Notification(playerSource, i18n.t("give_recovery", { garage = impound, price = recoveryPrice }), "success") end) -- ────────────────────────────────────────────── -- payVehiclePrice event -- ────────────────────────────────────────────── RegisterNetEvent("advancedgarages:server:payVehiclePrice") AddEventHandler("advancedgarages:server:payVehiclePrice", function(data) local playerSource = source local plate = data.vehicle.plate local balance = GetAccountMoney(playerSource, Config.MoneyType) local impoundDataRaw = MySQL.prepare.await( string.format("SELECT impound_data FROM %s WHERE plate = ?", garageTable), { plate } ) if not impoundDataRaw or impoundDataRaw == "" then Error("Vehicle not in impound in [server:payVehiclePrice]. Plate:", plate) return end Debug("impound_data", impoundDataRaw) local impoundData = json.decode(impoundDataRaw) local price = impoundData.price if balance < price then return Notification(playerSource, i18n.t("no_money", { price = price }), "info") end if not impoundData.getByPay then local timeLeft = os.difftime(impoundData.time, os.time()) if timeLeft > 0 then Error("getByPay is false and time is not expired but user trying to pay price. Probably trying to exploit the system. Plate:", plate, "source:", playerSource) return end end MySQL.Sync.execute( string.format("UPDATE %s SET impound_data = '' WHERE plate = ?", garageTable), { plate } ) RemoveAccountMoney(playerSource, Config.MoneyType, price) Notification(playerSource, i18n.t("impound_pay", { price = price }), "info") TriggerClientEvent("advancedgarages:client:payVehiclePrice", playerSource, plate) end) -- ────────────────────────────────────────────── -- updateVehicleState event (QB-Core) -- ────────────────────────────────────────────── RegisterNetEvent("advancedgarages:server:updateVehicleState") AddEventHandler("advancedgarages:server:updateVehicleState", function(newState, plate) MySQL.update( "UPDATE player_vehicles SET state = ?, depotprice = ? WHERE plate = ?", { newState, 0, plate } ) end) -- ────────────────────────────────────────────── -- AddGarage (from external resources) -- ────────────────────────────────────────────── RegisterNetEvent("advancedgarages:AddGarage") AddEventHandler("advancedgarages:AddGarage", function(garageName, garageData) if Config.Garages[garageName] then local invoker = GetInvokingResource() return Error("tried to create a garage with the name:", garageName, "but it already exists. Invoking resource:", invoker) end Config.Garages[garageName] = garageData TriggerClientEvent("garages:updateGarage", -1, garageData) end) -- ────────────────────────────────────────────── -- Resource name verification -- ────────────────────────────────────────────── CreateThread(function() local resourceName = GetCurrentResourceName() if resourceName == "qs-advancedgarages" then verify = true end if verify ~= true then repeat Wait(3000) Error("The console will close because ^4qs-advancedgarages ^0changed its name!") Wait(5000) os.exit() until verify == true end end)