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

690 lines
38 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- ============================================================
-- mercyv-garage | server/main.lua (komplett neu)
-- ============================================================
local ESX = nil
local GaragesData = {}
local SpawnedPlates = {}
local PersistSentTo = {} -- [src] = true
local PersistDone = false -- true sobald erster Spieler alle Fahrzeuge gespawnt hat
-- ──────────────────────────────────────────────────────────────
-- Framework
-- ──────────────────────────────────────────────────────────────
Citizen.CreateThread(function()
if Config.Framework == "esx" then
if Config.NewESX then
ESX = exports['es_extended']:getSharedObject()
else
while ESX == nil do
TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end)
Wait(0)
end
end
end
end)
local function GetPlayer(src)
if Config.Framework == "esx" then return ESX and ESX.GetPlayerFromId(src) end
end
local function GetIdentifier(src)
local xp = GetPlayer(src)
return xp and xp.identifier or nil
end
local function GetPlayerJob(src)
local xp = GetPlayer(src)
return xp and xp.job and xp.job.name or nil
end
local function GetPlayerMoney(src, t)
local xp = GetPlayer(src)
if not xp then return 0 end
return t == 'money' and xp.getMoney() or xp.getAccount(t).money
end
local function RemovePlayerMoney(src, t, amount)
local xp = GetPlayer(src)
if not xp then return end
if t == 'money' then xp.removeMoney(amount) else xp.removeAccountMoney(t, amount) end
end
local function vT() return Config.Framework == "esx" and "owned_vehicles" or "player_vehicles" end
local function oC() return Config.Framework == "esx" and "owner" or "citizenid" end
local function pC() return Config.Framework == "esx" and "vehicle" or "mods" end
-- ──────────────────────────────────────────────────────────────
-- Admin check
-- ──────────────────────────────────────────────────────────────
local function IsAdmin(src)
if IsPlayerAceAllowed(src, Config.AdminAce) then return true end
if Config.Framework == "esx" and ESX then
local xp = ESX.GetPlayerFromId(src)
if xp then
local g = xp.getGroup()
if g == "admin" or g == "superadmin" then return true end
end
end
return false
end
-- ──────────────────────────────────────────────────────────────
-- DB Row → Garage
-- ──────────────────────────────────────────────────────────────
local function DBRowToGarage(row)
return {
id=row.id, label=row.label, garage=row.type, access=row.access, gang=row.gang or "none",
blip={ show=row.blip_show==1, blipName=row.label, blipType=row.blip_type, blipColour=row.blip_colour },
npc={ npcModel=row.npc_model, npc={x=row.npc_x,y=row.npc_y,z=row.npc_z,w=row.npc_heading} },
car={
spawncar={x=row.spawn_x,y=row.spawn_y,z=row.spawn_z,w=row.spawn_heading},
garage={x=row.park_x,y=row.park_y,z=row.park_z},
showcar={x=row.showcar_x or row.spawn_x,y=row.showcar_y or row.spawn_y,
z=row.showcar_z or row.spawn_z,w=row.showcar_heading or row.spawn_heading},
},
camera={x=row.cam_x or row.spawn_x+5, y=row.cam_y or row.spawn_y+5,
z=row.cam_z or row.spawn_z+1, rotationX=0, rotationY=0, rotationZ=row.cam_rot_z or -20},
-- Job-Fahrzeuge (aus mercyv_job_vehicles Tabelle, wird separat geladen)
cars = {},
}
end
-- ──────────────────────────────────────────────────────────────
-- Tabellen erstellen
-- ──────────────────────────────────────────────────────────────
local function addColIfMissing(tbl, col, def)
local r = MySQL.query.await(string.format(
"SELECT COUNT(*) AS cnt FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='%s' AND COLUMN_NAME='%s'",tbl,col))
if r and r[1] and r[1].cnt == 0 then
pcall(function() MySQL.query.await(string.format("ALTER TABLE `%s` ADD COLUMN `%s` %s",tbl,col,def)) end)
print(string.format('[mercyv-garage] Spalte `%s`.`%s` angelegt.',tbl,col))
end
end
local function EnsureTables()
local tbl = vT()
MySQL.query.await([[CREATE TABLE IF NOT EXISTS `mercyv_garages` (
`id` VARCHAR(80) NOT NULL, `label` VARCHAR(100) NOT NULL DEFAULT 'Garage',
`type` VARCHAR(20) NOT NULL DEFAULT 'normal', `access` VARCHAR(50) NOT NULL DEFAULT 'none',
`gang` VARCHAR(50) NOT NULL DEFAULT 'none', `blip_show` TINYINT(1) NOT NULL DEFAULT 1,
`blip_type` INT NOT NULL DEFAULT 357, `blip_colour` INT NOT NULL DEFAULT 3,
`npc_model` VARCHAR(100) NOT NULL DEFAULT 'a_m_m_prolhost_01',
`npc_x` FLOAT NOT NULL DEFAULT 0, `npc_y` FLOAT NOT NULL DEFAULT 0,
`npc_z` FLOAT NOT NULL DEFAULT 0, `npc_heading` FLOAT NOT NULL DEFAULT 0,
`spawn_x` FLOAT NOT NULL DEFAULT 0, `spawn_y` FLOAT NOT NULL DEFAULT 0,
`spawn_z` FLOAT NOT NULL DEFAULT 0, `spawn_heading` FLOAT NOT NULL DEFAULT 0,
`park_x` FLOAT NOT NULL DEFAULT 0, `park_y` FLOAT NOT NULL DEFAULT 0,
`park_z` FLOAT NOT NULL DEFAULT 0,
`showcar_x` FLOAT DEFAULT NULL, `showcar_y` FLOAT DEFAULT NULL,
`showcar_z` FLOAT DEFAULT NULL, `showcar_heading` FLOAT DEFAULT 0,
`cam_x` FLOAT DEFAULT NULL, `cam_y` FLOAT DEFAULT NULL,
`cam_z` FLOAT DEFAULT NULL, `cam_rot_z` FLOAT DEFAULT -20,
PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4]])
addColIfMissing(tbl, 'veh_class', 'TINYINT DEFAULT 0')
addColIfMissing(tbl, 'last_coords', 'VARCHAR(255) DEFAULT NULL')
addColIfMissing(tbl, 'favorite', "VARCHAR(10) DEFAULT '0'")
local count = MySQL.query.await('SELECT COUNT(*) AS cnt FROM mercyv_garages')
if count and count[1] and count[1].cnt == 0 then
print('[mercyv-garage] Tabelle leer trage Standard-Garagen ein...')
SeedDefaultGarages()
end
print('[mercyv-garage] Tabellen OK.')
end
-- ──────────────────────────────────────────────────────────────
-- Seed Default Garagen
-- ──────────────────────────────────────────────────────────────
local function SeedDefaultGarages()
local garages = {
{"Garage A","Garage A","normal","none",1,357,3,"a_m_m_prolhost_01",214.5806,-806.8969,30.8052,336.3445,233.7616,-802.9507,30.4636,71.0069,214.9846,-790.6285,30.8301,236.39,-779.89,30.67,161.68,234.57,-785.1,30.59,-20.0},
{"Garage B","Garage B","normal","none",1,357,3,"a_m_m_prolhost_01",275.95,-344.06,45.17,165.24,292.79,-332.22,44.92,161.25,271.68,-341.61,44.92,274.63,-330.28,44.70,164.27,273.08,-335.04,44.92,-20.0},
{"Garage D","Garage D","normal","none",1,357,3,"a_m_m_prolhost_01",68.35,13.85,69.21,167.77,73.24,11.78,68.85,155.92,73.24,11.78,68.85,59.35,24.31,69.73,245.08,64.78,22.19,69.54,70.0},
{"Garage E","Garage E","normal","none",1,357,3,"a_m_m_prolhost_01",363.48,296.86,103.50,244.81,367.98,296.57,103.42,345.36,367.98,296.57,103.42,376.33,288.82,103.20,69.26,371.18,290.66,103.31,-110.0},
{"Garage F","Garage F","normal","none",1,357,3,"a_m_m_prolhost_01",-1158.51,-740.67,19.89,41.16,-1169.03,-743.49,19.63,42.38,-1169.03,-743.49,19.63,-1145.2,-759.03,18.82,39.92,-1148.57,-754.86,18.97,-140.0},
{"Garage G","Garage G","normal","none",1,357,3,"a_m_m_prolhost_01",-795.33,-2023.8,9.17,66.37,-790.11,-2022.68,8.87,58.85,-791.39,-2030.26,8.87,-763.11,-2042.28,8.91,37.29,-766.54,-2037.82,8.9,-143.0},
{"Garage H","Garage H","normal","none",1,357,3,"a_m_m_prolhost_01",-468.87,-819.67,30.52,358.04,-472.16,-812.83,30.53,179.63,-453.49,-814.23,30.58,-472.02,-800.43,30.54,183.47,-472.16,-806.15,30.54,-3.0},
{"Garage I","Garage I","normal","none",1,357,3,"a_m_m_prolhost_01",1142.38,2661.28,38.16,92.19,1137.57,2674.86,38.25,1.08,1137.59,2653.02,38.0,1121.15,2665.03,38.02,266.97,1127.68,2664.84,38.02,88.0},
{"Garage J","Garage J","normal","none",1,357,3,"a_m_m_prolhost_01",83.51,6420.3,31.76,313.17,85.93,6426.8,31.34,38.93,79.68,6417.33,31.28,112.65,6396.47,31.31,42.5,107.37,6402.14,31.33,-138.0},
{"Garage K","Boot Garage","boat","none",1,356,3,"a_m_m_prolhost_01",-717.9,-1327.46,1.6,50.86,-718.05,-1334.24,-0.44,222.71,-718.03,-1334.21,1.0,-723.7,-1329.22,-0.11,229.03,-719.57,-1332.72,1.41,50.0},
{"Garage L","Flugzeug Garage","aircraft","none",1,359,3,"a_m_m_prolhost_01",-1251.69,-3399.94,13.94,59.19,-1246.91,-3355.14,13.95,330.68,-1246.91,-3355.14,13.95,-1273.01,-3402.28,13.94,331.01,-1268.42,-3394.32,13.94,-210.0},
{"Garage M","Garage M","normal","none",1,357,3,"a_m_m_prolhost_01",271.94,-1509.32,29.18,87.30,243.22,-1502.84,29.14,222.92,243.22,-1502.84,29.14,253.68,-1511.65,29.14,260.14,256.95,-1500.42,29.14,-200.0},
{"Garage N","Garage N","normal","none",1,357,3,"a_m_m_prolhost_01",-1134.78,2682.73,18.46,132.28,-1155.64,2665.05,18.09,223.0,-1141.49,2680.13,18.09,-1157.10,2672.85,18.09,175.18,-1145.82,2670.54,19.75,-280.0},
{"Garage T","Garage T","normal","none",1,357,3,"a_m_m_prolhost_01",302.3,-189.94,61.57,73.01,288.09,-194.55,61.57,249.13,301.1,-183.12,61.59,274.64,-189.47,61.57,252.0,279.92,-191.36,61.57,61.57},
{"Impound Garage","Abschlepphof","impound","none",1,68,3,"a_m_m_prolhost_01",406.88,-1625.23,29.29,229.89,408.0,-1645.66,29.29,228.92,408.0,-1645.66,29.29,401.4,-1639.93,29.29,230.79,406.0,-1643.48,29.29,50.29},
{"Impound Boat","Abschlepphof Boot","impoundboat","none",1,357,3,"a_m_m_prolhost_01",-769.64,-1425.65,1.60,230.0,-786.56,-1424.55,-0.51,133.50,-786.56,-1424.55,-0.51,-786.56,-1424.55,-0.51,133.50,-795.95,-1436.94,3.06,322.48},
{"Impound Plane","Abschlepphof Flugzeug","impoundplane","none",1,357,3,"a_m_m_prolhost_01",-1030.27,-3016.30,13.95,339.01,-979.81,-2995.32,13.95,69.51,-979.81,-2995.32,13.95,-984.89,-3012.54,13.95,61.43,-994.19,-3008.20,13.95,236.98},
{"police","Polizei Garage","jobgarage","police",0,357,3,"ig_solomon",457.60,-977.66,21.95,87.43,449.80,-971.60,21.45,177.42,449.42,-979.14,21.45,417.47,-974.65,21.45,177.18,428.37,-974.73,21.45,-270.0},
{"ambulance","Ambulanz Garage","jobgarage","ambulance",0,357,3,"ig_solomon",-286.81,-588.56,27.78,1.87,-286.29,-576.06,27.63,85.73,-285.72,-580.70,27.63,-305.70,-567.67,27.63,296.93,-311.61,-563.86,27.63,-110.0},
}
local q = [[INSERT IGNORE INTO mercyv_garages
(id,label,type,access,blip_show,blip_type,blip_colour,npc_model,
npc_x,npc_y,npc_z,npc_heading,spawn_x,spawn_y,spawn_z,spawn_heading,
park_x,park_y,park_z,showcar_x,showcar_y,showcar_z,showcar_heading,
cam_x,cam_y,cam_z,cam_rot_z) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)]]
for _,g in ipairs(garages) do
MySQL.update.await(q,{g[1],g[2],g[3],g[4],g[5],g[6],g[7],g[8],g[9],g[10],g[11],g[12],g[13],g[14],g[15],g[16],g[17],g[18],g[19],g[20],g[21],g[22],g[23],g[24],g[25],g[26],g[27]})
end
print('[mercyv-garage] Standard-Garagen eingetragen.')
end
-- ──────────────────────────────────────────────────────────────
-- Garagen laden
-- ──────────────────────────────────────────────────────────────
local function LoadGarages()
local result = MySQL.query.await('SELECT * FROM mercyv_garages ORDER BY id')
GaragesData = {}
if result then
for _,row in ipairs(result) do
GaragesData[row.id] = DBRowToGarage(row)
end
end
local cnt = 0; for _ in pairs(GaragesData) do cnt=cnt+1 end
print(string.format('[mercyv-garage] %d Garagen geladen.', cnt))
end
-- ──────────────────────────────────────────────────────────────
-- Startup
-- ──────────────────────────────────────────────────────────────
AddEventHandler('onResourceStart', function(res)
if res ~= GetCurrentResourceName() then return end
Wait(1500)
EnsureTables()
-- Blip-Fix VOR LoadGarages damit die Daten korrekt sind
MySQL.update.await("UPDATE mercyv_garages SET blip_show=1 WHERE type NOT IN ('jobgarage','impound','impoundboat','impoundplane')")
LoadGarages()
local tbl = vT()
-- Fahrzeuge OHNE gespeicherte Position → zurück in Garage
local noPos = MySQL.query.await(string.format(
"SELECT COUNT(*) AS cnt FROM `%s` WHERE stored=0 AND (last_coords IS NULL OR last_coords='')", tbl))
if noPos and noPos[1] and noPos[1].cnt > 0 then
-- Fahrzeuge ohne parking → Standard-Garage setzen
MySQL.update.await(string.format(
"UPDATE `%s` SET stored=1 WHERE stored=0 AND (last_coords IS NULL OR last_coords='')", tbl))
-- Fahrzeuge ohne parking-Eintrag → Garage A als Fallback
MySQL.update.await(string.format(
"UPDATE `%s` SET parking='Garage A' WHERE stored=1 AND (parking IS NULL OR parking='')", tbl))
print(string.format('[mercyv-garage] %d Fahrzeuge ohne Position → zurück in Garage.', noPos[1].cnt))
end
-- Fahrzeuge MIT gespeicherter Position → werden nach Join gespawnt
local withPos = MySQL.query.await(string.format(
"SELECT COUNT(*) AS cnt FROM `%s` WHERE stored=0 AND last_coords IS NOT NULL AND last_coords!=''", tbl))
local spawnCount = withPos and withPos[1] and withPos[1].cnt or 0
print(string.format('[mercyv-garage] %d Fahrzeuge mit gespeicherter Position → werden gespawnt.', spawnCount))
SpawnedPlates = {}
PersistSentTo = {}
PersistDone = false
end)
-- ──────────────────────────────────────────────────────────────
-- Broadcast
-- ──────────────────────────────────────────────────────────────
local function BroadcastGarages()
TriggerClientEvent('mercyv-garage:syncGarages', -1, GaragesData)
end
RegisterNetEvent('mercyv-garage:requestGarages', function()
TriggerClientEvent('mercyv-garage:syncGarages', source, GaragesData)
end)
RegisterNetEvent('mercyv-garage:clientReady', function()
local src = source
-- Persist-Lock SOFORT setzen (synchron, außerhalb des Threads)
local shouldSpawn = false
if not PersistDone then
PersistDone = true
shouldSpawn = true
PersistSentTo[src] = true
print(string.format('[mercyv-garage Persist] Client %d wird Fahrzeuge spawnen.', src))
end
Citizen.CreateThread(function()
Wait(500)
TriggerClientEvent('mercyv-garage:syncGarages', src, GaragesData)
Wait(200)
TriggerClientEvent('mercyv-garage:setAdminStatus', src, IsAdmin(src))
Wait(200)
if shouldSpawn then
SendPersistList(src)
end
end)
end)
RegisterNetEvent('mercyv-garage:checkAdminStatus', function()
TriggerClientEvent('mercyv-garage:setAdminStatus', source, IsAdmin(source))
end)
-- ──────────────────────────────────────────────────────────────
-- Persist: Spawn-Liste an Client
-- ──────────────────────────────────────────────────────────────
function SendPersistList(src)
-- Alle Fahrzeuge in der Welt löschen die stored=0 sind (Persist-Fahrzeuge)
-- So entstehen keine Duplikate wenn Resource neu gestartet wird
local tblClean = vT()
local storedOut = MySQL.query.await(string.format(
"SELECT plate FROM `%s` WHERE stored=0 AND last_coords IS NOT NULL AND last_coords!=''", tblClean))
if storedOut and #storedOut > 0 then
local outPlates = {}
for _, row in ipairs(storedOut) do
outPlates[string.lower(string.gsub(row.plate or '', '%s+', ''))] = true
end
local allVehs = GetAllVehicles()
local deleted = 0
for _, veh in ipairs(allVehs) do
if DoesEntityExist(veh) then
local vPlate = string.lower(string.gsub(GetVehicleNumberPlateText(veh) or '', '%s+', ''))
if outPlates[vPlate] then
DeleteEntity(veh)
deleted = deleted + 1
end
end
end
if deleted > 0 then
print(string.format('[mercyv-garage Persist] %d alte Persist-Fahrzeuge gelöscht.', deleted))
end
end
SpawnedPlates = {}
local tbl = vT()
local props = pC()
local results = MySQL.query.await(string.format(
"SELECT plate, `%s` AS props, last_coords, veh_class FROM `%s` WHERE stored=0 AND last_coords IS NOT NULL AND last_coords!=''",
props, tbl))
print(string.format('[mercyv-garage Persist] Fahrzeuge mit Koordinaten: %d', results and #results or 0))
if not results or #results == 0 then return end
local toSpawn = {}
for _,row in ipairs(results) do
local np = string.lower(string.gsub(row.plate or '','%s+',''))
if not SpawnedPlates[np] then
local ok, coords = pcall(json.decode, row.last_coords)
if ok and coords and coords.x then
local modelname = ''
if row.props then
local okP,pd = pcall(json.decode, row.props)
if okP and pd and pd.model then modelname = tostring(pd.model) end
end
table.insert(toSpawn, {
plate=row.plate, modelname=modelname, props=row.props,
vehClass=row.veh_class or 0,
x=coords.x, y=coords.y, z=coords.z, heading=coords.w or 0,
})
end
end
end
if #toSpawn > 0 then
print(string.format('[mercyv-garage Persist] Sende %d Fahrzeuge an Client %d', #toSpawn, src))
TriggerClientEvent('mercyv-garage:persistSpawn', src, toSpawn)
end
end
RegisterNetEvent('mercyv-garage:persistMarkSpawned', function(plates)
for _,p in ipairs(plates) do SpawnedPlates[p] = true end
PersistDone = true -- Fahrzeuge sind in der Welt, kein weiterer Spawn nötig
print('[mercyv-garage Persist] Persist abgeschlossen, keine weiteren Spawns.')
end)
-- ──────────────────────────────────────────────────────────────
-- Koordinaten speichern
-- ──────────────────────────────────────────────────────────────
RegisterNetEvent('mercyv-garage:saveCoords', function(list)
if type(list) ~= 'table' then return end
local tbl = vT()
for _,v in ipairs(list) do
if v.plate and v.x then
MySQL.update.await(
string.format("UPDATE `%s` SET last_coords=? WHERE plate=? AND stored=0", tbl),
{ json.encode({x=math.floor(v.x*100)/100, y=math.floor(v.y*100)/100,
z=math.floor(v.z*100)/100, w=math.floor((v.heading or 0)*100)/100}), v.plate })
end
end
end)
RegisterNetEvent('mercyv-garage:requestOutsidePlates', function()
local src = source
local tbl = vT()
local r = MySQL.query.await(string.format("SELECT plate FROM `%s` WHERE stored=0", tbl))
local plates = {}
if r then for _,row in ipairs(r) do plates[#plates+1] = row.plate end end
TriggerClientEvent('mercyv-garage:outsidePlates', src, plates)
end)
-- ──────────────────────────────────────────────────────────────
-- Fahrzeuge abfragen (normale + Job-Garagen)
-- ──────────────────────────────────────────────────────────────
RegisterNetEvent('mercyv-garage:getVehicles', function(garageId)
local src = source
local g = GaragesData[garageId]
if not g then return end
-- Job-Garage: Job-Fahrzeuge aus Config zurückgeben
if g.garage == 'jobgarage' then
local playerJob = GetPlayerJob(src)
if playerJob ~= g.access then
Config.ServerNotification(src, Config.Notify.NO_ACCESS, "error")
return
end
-- Job-Fahrzeuge aus Config.JobVehicles
local jobCars = {}
local jobList = Config.JobVehicles and Config.JobVehicles[g.access] or {}
for i, car in ipairs(jobList) do
table.insert(jobCars, {
plate = car.model,
props = nil,
modelname= car.model,
stored = 1,
parking = garageId,
favorite = 0,
vehClass = 0,
isJobVehicle = true,
label = car.label or car.model,
jobGrade = car.grade or 0,
})
end
TriggerClientEvent('mercyv-garage:receiveVehicles', src, jobCars, garageId)
return
end
-- Normale Garage: persönliche Fahrzeuge
local identifier = GetIdentifier(src)
if not identifier then return end
local tbl = vT(); local owner = oC(); local props = pC()
local query = string.format(
"SELECT plate, `%s` AS props, stored, parking, favorite, veh_class FROM `%s` WHERE `%s`=?",
props, tbl, owner)
local result = MySQL.query.await(query, {identifier})
local vehicles = {}
if result then
for _,row in ipairs(result) do
local gt = g.garage
if gt=='impound' or gt=='impoundboat' or gt=='impoundplane' then
-- Impound: nur eingelagerte Fahrzeuge anzeigen die explizit diesem Impound zugewiesen sind
if row.parking ~= garageId or row.stored ~= 1 then goto cont end
end
local modelname = ''
if row.props then
local ok,pd = pcall(json.decode, row.props)
if ok and pd and pd.model then modelname = tostring(pd.model) end
end
table.insert(vehicles, {
plate=row.plate, props=row.props, modelname=modelname,
stored=row.stored or 1, parking=row.parking or '',
favorite=row.favorite or 0, vehClass=row.veh_class or 0,
})
::cont::
end
end
TriggerClientEvent('mercyv-garage:receiveVehicles', src, vehicles, garageId)
end)
-- ──────────────────────────────────────────────────────────────
-- Fahrzeug holen
-- ──────────────────────────────────────────────────────────────
RegisterNetEvent('mercyv-garage:takeOut', function(plate, garageId, vehClass)
local src = source
local g = GaragesData[garageId]
if not g then return end
-- Job-Fahrzeug
if g.garage == 'jobgarage' then
local playerJob = GetPlayerJob(src)
if playerJob ~= g.access then
Config.ServerNotification(src, Config.Notify.NO_ACCESS, "error"); return
end
-- Einfach spawnen, kein DB-Eintrag nötig
TriggerClientEvent('mercyv-garage:doSpawn', src, {
plate=plate, props=nil, modelname=plate, garageId=garageId, isJobVehicle=true
})
return
end
-- Impound-Gebühr
if g.garage=='impound' or g.garage=='impoundboat' or g.garage=='impoundplane' then
if GetPlayerMoney(src, Config.MoneyType) < Config.ImpoundPrice then
Config.ServerNotification(src, Config.Notify.NO_MONEY, "error"); return
end
RemovePlayerMoney(src, Config.MoneyType, Config.ImpoundPrice)
Config.ServerNotification(src, Config.Notify.IMPOUND_PAID, "success")
end
local identifier = GetIdentifier(src)
if not identifier then return end
local tbl=vT(); local owner=oC(); local props=pC()
local result = MySQL.query.await(
string.format("SELECT plate, `%s` AS props, stored FROM `%s` WHERE plate=? AND `%s`=?", props, tbl, owner),
{plate, identifier})
if not result or not result[1] then
Config.ServerNotification(src, Config.Notify.NOT_OWNED, "error"); return
end
local row = result[1]
-- last_coords wird vom Client gesetzt sobald Fahrzeug sich bewegt
-- NULL = noch keine Position gespeichert (Migration setzt es auf stored=1 bei Neustart)
-- parking bleibt gesetzt (wichtig für Restart-Recovery)
-- Impound-Filter prüft stored=1 UND parking=garageId → stored=0 wird nicht angezeigt
MySQL.update.await(
string.format("UPDATE `%s` SET stored=0, veh_class=?, last_coords=NULL WHERE plate=? AND `%s`=?", tbl, owner),
{vehClass or 0, plate, identifier})
SpawnedPlates[string.lower(string.gsub(plate,'%s+',''))] = true
local modelname = ''
if row.props then
local ok,pd = pcall(json.decode, row.props)
if ok and pd and pd.model then modelname = tostring(pd.model) end
end
TriggerClientEvent('mercyv-garage:doSpawn', src, {
plate=plate, props=row.props, modelname=modelname, garageId=garageId
})
end)
-- ──────────────────────────────────────────────────────────────
-- Einparken
-- ──────────────────────────────────────────────────────────────
RegisterNetEvent('mercyv-garage:parkIn', function(plate, garageId, propsJson, vehClass)
local src = source
local g = GaragesData[garageId]
-- Job-Fahrzeuge werden einfach gelöscht (kein DB-Eintrag)
if g and g.garage == 'jobgarage' then
Config.ServerNotification(src, Config.Notify.PARKED_IN, "success"); return
end
local identifier = GetIdentifier(src)
if not identifier then return end
local tbl=vT(); local owner=oC(); local props=pC()
local r = MySQL.query.await(string.format("SELECT plate FROM `%s` WHERE plate=? AND `%s`=?",tbl,owner),{plate,identifier})
if not r or not r[1] then Config.ServerNotification(src, Config.Notify.NOT_OWNED, "error"); return end
MySQL.update.await(
string.format("UPDATE `%s` SET stored=1, parking=?, `%s`=?, veh_class=?, last_coords=NULL WHERE plate=? AND `%s`=?",tbl,props,owner),
{garageId, propsJson, vehClass or 0, plate, identifier})
SpawnedPlates[string.lower(string.gsub(plate,'%s+',''))] = nil
Config.ServerNotification(src, Config.Notify.PARKED_IN, "success")
end)
-- ──────────────────────────────────────────────────────────────
-- Favorit
-- ──────────────────────────────────────────────────────────────
RegisterNetEvent('mercyv-garage:setFavorite', function(plate, value)
local src=source; local id=GetIdentifier(src)
if not id then return end
local tbl=vT(); local owner=oC()
MySQL.update(string.format("UPDATE `%s` SET favorite=? WHERE plate=? AND `%s`=?",tbl,owner),{value,plate,id})
end)
-- ──────────────────────────────────────────────────────────────
-- Admin: Garagen-Liste
-- ──────────────────────────────────────────────────────────────
RegisterNetEvent('mercyv-garage:admin:requestGarages', function()
local src=source
local list={}
for id,g in pairs(GaragesData) do
table.insert(list,{
id=id,label=g.label or id,type=g.garage,access=g.access or "none",
npc_model=g.npc and g.npc.npcModel or "a_m_m_prolhost_01",
npc_x=g.npc and g.npc.npc and g.npc.npc.x or 0,
npc_y=g.npc and g.npc.npc and g.npc.npc.y or 0,
npc_z=g.npc and g.npc.npc and g.npc.npc.z or 0,
npc_heading=g.npc and g.npc.npc and g.npc.npc.w or 0,
spawn_x=g.car and g.car.spawncar and g.car.spawncar.x or 0,
spawn_y=g.car and g.car.spawncar and g.car.spawncar.y or 0,
spawn_z=g.car and g.car.spawncar and g.car.spawncar.z or 0,
spawn_heading=g.car and g.car.spawncar and g.car.spawncar.w or 0,
park_x=g.car and g.car.garage and g.car.garage.x or 0,
park_y=g.car and g.car.garage and g.car.garage.y or 0,
park_z=g.car and g.car.garage and g.car.garage.z or 0,
showcar_x=g.car and g.car.showcar and g.car.showcar.x,
showcar_y=g.car and g.car.showcar and g.car.showcar.y,
showcar_z=g.car and g.car.showcar and g.car.showcar.z,
showcar_heading=g.car and g.car.showcar and g.car.showcar.w or 0,
cam_x=g.camera and g.camera.x, cam_y=g.camera and g.camera.y,
cam_z=g.camera and g.camera.z, cam_rot_z=g.camera and g.camera.rotationZ or -20,
blip_show=g.blip and g.blip.show and 1 or 0,
blip_type=g.blip and g.blip.blipType or 357,
blip_colour=g.blip and g.blip.blipColour or 3,
})
end
TriggerClientEvent('mercyv-garage:admin:receiveGarages', src, list)
end)
RegisterNetEvent('mercyv-garage:admin:saveGarage', function(data)
local src=source
if not IsAdmin(src) then Config.ServerNotification(src,Config.Notify.ADMIN_NO_PERM,"error"); return end
if not data or not data.id or data.id=="" then return end
MySQL.update([[INSERT INTO mercyv_garages
(id,label,type,access,gang,blip_show,blip_type,blip_colour,npc_model,
npc_x,npc_y,npc_z,npc_heading,spawn_x,spawn_y,spawn_z,spawn_heading,
park_x,park_y,park_z,showcar_x,showcar_y,showcar_z,showcar_heading,
cam_x,cam_y,cam_z,cam_rot_z)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE
label=VALUES(label),type=VALUES(type),access=VALUES(access),gang=VALUES(gang),
blip_show=VALUES(blip_show),blip_type=VALUES(blip_type),blip_colour=VALUES(blip_colour),
npc_model=VALUES(npc_model),npc_x=VALUES(npc_x),npc_y=VALUES(npc_y),
npc_z=VALUES(npc_z),npc_heading=VALUES(npc_heading),
spawn_x=VALUES(spawn_x),spawn_y=VALUES(spawn_y),spawn_z=VALUES(spawn_z),spawn_heading=VALUES(spawn_heading),
park_x=VALUES(park_x),park_y=VALUES(park_y),park_z=VALUES(park_z),
showcar_x=VALUES(showcar_x),showcar_y=VALUES(showcar_y),showcar_z=VALUES(showcar_z),showcar_heading=VALUES(showcar_heading),
cam_x=VALUES(cam_x),cam_y=VALUES(cam_y),cam_z=VALUES(cam_z),cam_rot_z=VALUES(cam_rot_z)]],
{data.id,data.label,data.type,data.access or "none",data.gang or "none",
(data.type=='jobgarage' and (data.blip_show and 1 or 0) or 1),data.blip_type or 357,data.blip_colour or 3,
data.npc_model or "a_m_m_prolhost_01",
data.npc_x,data.npc_y,data.npc_z,data.npc_heading or 0,
data.spawn_x,data.spawn_y,data.spawn_z,data.spawn_heading or 0,
data.park_x,data.park_y,data.park_z,
data.showcar_x,data.showcar_y,data.showcar_z,data.showcar_heading or 0,
data.cam_x,data.cam_y,data.cam_z,data.cam_rot_z or -20})
GaragesData[data.id] = DBRowToGarage({
id=data.id,label=data.label,type=data.type,access=data.access or "none",gang=data.gang or "none",
blip_show=(data.type=='jobgarage' and (data.blip_show and 1 or 0) or 1),blip_type=data.blip_type or 357,blip_colour=data.blip_colour or 3,
npc_model=data.npc_model or "a_m_m_prolhost_01",
npc_x=data.npc_x,npc_y=data.npc_y,npc_z=data.npc_z,npc_heading=data.npc_heading or 0,
spawn_x=data.spawn_x,spawn_y=data.spawn_y,spawn_z=data.spawn_z,spawn_heading=data.spawn_heading or 0,
park_x=data.park_x,park_y=data.park_y,park_z=data.park_z,
showcar_x=data.showcar_x,showcar_y=data.showcar_y,showcar_z=data.showcar_z,showcar_heading=data.showcar_heading or 0,
cam_x=data.cam_x,cam_y=data.cam_y,cam_z=data.cam_z,cam_rot_z=data.cam_rot_z or -20,
})
BroadcastGarages()
Config.ServerNotification(src, Config.Notify.ADMIN_SAVED, "success")
end)
RegisterNetEvent('mercyv-garage:admin:deleteGarage', function(garageId)
local src=source
if not IsAdmin(src) then Config.ServerNotification(src,Config.Notify.ADMIN_NO_PERM,"error"); return end
MySQL.update("DELETE FROM mercyv_garages WHERE id=?",{garageId})
GaragesData[garageId]=nil
BroadcastGarages()
Config.ServerNotification(src,Config.Notify.ADMIN_DELETED,"success")
end)
-- ──────────────────────────────────────────────────────────────
-- Fahrzeug zerstört → zurück in Garage
-- ──────────────────────────────────────────────────────────────
RegisterNetEvent('mercyv-garage:vehicleDestroyed', function(plate)
local src = source
local identifier = GetIdentifier(src)
if not identifier or not plate then return end
local tbl = vT()
local owner = oC()
-- Nur wenn das Fahrzeug dem Spieler gehört und draußen war
local r = MySQL.query.await(
string.format("SELECT plate, parking FROM `%s` WHERE plate=? AND `%s`=? AND stored=0", tbl, owner),
{ plate, identifier })
if r and r[1] then
-- Zurück in die letzte Garage oder Garage A als Fallback
local lastParking = r[1].parking
if not lastParking or lastParking == '' then lastParking = 'Garage A' end
MySQL.update.await(
string.format("UPDATE `%s` SET stored=1, last_coords=NULL WHERE plate=? AND `%s`=?", tbl, owner),
{ plate, identifier })
SpawnedPlates[string.lower(string.gsub(plate, '%s+', ''))] = nil
Config.ServerNotification(src,
"Dein Fahrzeug (" .. plate .. ") wurde eingelagert.", "error")
print(string.format('[mercyv-garage] Fahrzeug %s zerstört → stored=1', plate))
end
end)
-- ──────────────────────────────────────────────────────────────
-- /dv → Fahrzeug in Impound schicken
-- ──────────────────────────────────────────────────────────────
RegisterNetEvent('mercyv-garage:sendToImpound', function(plate)
local src = source
local identifier = GetIdentifier(src)
if not identifier then return end
local tbl = vT()
local owner = oC()
-- Impound-Garage ID finden
local impoundId = nil
for id, g in pairs(GaragesData) do
if g.garage == 'impound' then
impoundId = id
break
end
end
if not impoundId then
-- Kein Impound konfiguriert → einfach stored=1
MySQL.update(
string.format("UPDATE `%s` SET stored=1, last_coords=NULL WHERE plate=? AND `%s`=?", tbl, owner),
{ plate, identifier })
return
end
MySQL.update(
string.format("UPDATE `%s` SET stored=1, parking=?, last_coords=NULL WHERE plate=? AND `%s`=?", tbl, owner),
{ impoundId, plate, identifier })
Config.ServerNotification(src, "Fahrzeug wurde abgeschleppt. Impound: " .. impoundId, "error")
print(string.format('[mercyv-garage] /dv: %s von %s → Impound (%s)', plate, identifier, impoundId))
end)
-- Reset PersistDone wenn der Spawn-Spieler disconnectet
AddEventHandler('playerDropped', function(reason)
local src = source
if PersistSentTo[src] then
PersistSentTo[src] = nil
-- Wenn noch kein anderer Spieler gespawnt hat, Reset
local anyOther = false
for k,_ in pairs(PersistSentTo) do anyOther = true; break end
if not anyOther then
PersistDone = false
print('[mercyv-garage Persist] Spawn-Spieler hat Server verlassen, Reset.')
end
end
end)