1419 lines
60 KiB
Lua
1419 lines
60 KiB
Lua
-- ============================================================
|
||
-- 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)
|